From 77ae8d164152ac782dacb3f3f3f1ac5ac71eb4b8 Mon Sep 17 00:00:00 2001
From: Tom Robinson <tlrobinson@gmail.com>
Date: Mon, 21 Oct 2019 16:48:13 -0700
Subject: [PATCH] Fix Google Analytics query builder crashes (#11186)

* hide section picker if when viewing column settings

* hide sidebar title

* add ChartSettingsSidebar test

* always show column settings title

* lint

* override sidebar title

* remove unneeded diff

* group reference sidebar tables by schema

* keep everything as one list

* use name instead of display_name

* add tests

* add schema pane

* filter to querable tables

* update tests

* unused imports

* sort schema and table names

* change scalar compact formatting to depend on pixel width rather than grid width (#10932)

* prompt for save when sharing unsaved question (#10976)

* use generic props override instead of just `title` and `onBack`

* Update pulse table style to match app (#10989)

* undefined -> null, you can spread null aparently

* Upgrade redshift driver to 1.2.36.1060 (#11181)

* fix bug where column settings were dropped (#11154)

* remove leading slash on publicPath (#11174)

* Fix GA crashes
---
 .../src/metabase-lib/lib/metadata/Metric.js   | 24 +++++-
 .../lib/queries/structured/Aggregation.js     |  2 +-
 .../containers/DashboardEmbedWidget.jsx       | 64 +++++++++++----
 .../public/components/widgets/EmbedWidget.jsx | 78 ------------------
 .../query_builder/components/QueryModals.jsx  | 24 ++++++
 .../components/dataref/DataReference.jsx      |  6 +-
 .../components/dataref/DatabasePane.jsx       | 36 ++-------
 .../dataref/DatabaseSchemasPane.jsx           | 44 +++++++++++
 .../components/dataref/DatabaseTablesPane.jsx | 45 +++++++++++
 .../components/dataref/MainPane.jsx           |  1 +
 .../components/dataref/SchemaPane.jsx         | 46 +++++++++++
 .../components/view/ViewFooter.jsx            | 14 ++--
 .../view/sidebars/ChartSettingsSidebar.jsx    | 79 +++++++++++--------
 .../containers/QuestionEmbedWidget.jsx        | 34 ++++++--
 frontend/src/metabase/services.js             | 27 ++++---
 .../components/ChartSettings.jsx              | 20 ++++-
 .../settings/ChartNestedSettingColumns.jsx    | 57 +++++++------
 .../metabase/visualizations/lib/apply_axis.js |  4 +-
 .../visualizations/visualizations/Scalar.jsx  | 10 ++-
 .../__support__/sample_dataset_fixture.js     |  6 +-
 .../lib/metadata/Metric.unit.spec.js          | 55 +++++++++++++
 .../test/metabase/public/public.e2e.spec.js   |  5 +-
 .../dataref/DataReference.unit.spec.js        | 60 ++++++++++++++
 .../ChartSettingsSidebar.unit.spec.js         | 27 +++++++
 .../components/ChartSettings.unit.spec.js     | 39 +++++++++
 .../LineAreaBarRenderer.unit.spec.js          | 42 +++++++++-
 .../visualizations/Scalar.unit.spec.js        |  4 +-
 modules/drivers/redshift/project.clj          |  4 +-
 .../redshift/resources/metabase-plugin.yaml   |  2 +-
 src/metabase/pulse/render/style.clj           | 14 +++-
 src/metabase/pulse/render/table.clj           | 18 +++--
 webpack.config.js                             |  4 +-
 32 files changed, 659 insertions(+), 236 deletions(-)
 delete mode 100644 frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
 create mode 100644 frontend/src/metabase/query_builder/components/dataref/DatabaseSchemasPane.jsx
 create mode 100644 frontend/src/metabase/query_builder/components/dataref/DatabaseTablesPane.jsx
 create mode 100644 frontend/src/metabase/query_builder/components/dataref/SchemaPane.jsx
 create mode 100644 frontend/test/metabase-lib/lib/metadata/Metric.unit.spec.js
 create mode 100644 frontend/test/metabase/query_builder/components/dataref/DataReference.unit.spec.js
 create mode 100644 frontend/test/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.unit.spec.js

diff --git a/frontend/src/metabase-lib/lib/metadata/Metric.js b/frontend/src/metabase-lib/lib/metadata/Metric.js
index 7c066e54c0d..be3fb65c3f5 100644
--- a/frontend/src/metabase-lib/lib/metadata/Metric.js
+++ b/frontend/src/metabase-lib/lib/metadata/Metric.js
@@ -23,12 +23,32 @@ export default class Metric extends Base {
     return ["metric", this.id];
   }
 
+  /** Underlying query for this metric */
   definitionQuery() {
-    return this.table.query().setQuery(this.definition);
+    return this.definition
+      ? this.table.query().setQuery(this.definition)
+      : null;
   }
 
+  /** Underlying aggregation clause for this metric */
   aggregation() {
-    return this.definitionQuery().aggregations()[0];
+    const query = this.definitionQuery();
+    if (query) {
+      return query.aggregations()[0];
+    }
+  }
+
+  /** Column name when this metric is used in a query */
+  columnName() {
+    const aggregation = this.aggregation();
+    if (aggregation) {
+      return aggregation.columnName();
+    } else if (typeof this.id === "string") {
+      // special case for Google Analytics metrics
+      return this.id;
+    } else {
+      return null;
+    }
   }
 
   isActive(): boolean {
diff --git a/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js b/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js
index a442e4ff569..62efc423b8f 100644
--- a/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js
+++ b/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js
@@ -97,7 +97,7 @@ export default class Aggregation extends MBQLClause {
       const metric = aggregation.metric();
       if (metric) {
         // delegate to the metric's definition
-        return metric.aggregation().columnName();
+        return metric.columnName();
       }
     } else if (aggregation.isStandard()) {
       const short = this.short();
diff --git a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
index b930f17deda..024bda39f7f 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
@@ -2,10 +2,17 @@
 
 import React, { Component } from "react";
 import { connect } from "react-redux";
+import cx from "classnames";
+import { t } from "ttag";
 
-import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
+import Tooltip from "metabase/components/Tooltip";
+import Icon from "metabase/components/Icon";
+import ModalWithTrigger from "metabase/components/ModalWithTrigger";
+
+import EmbedModalContent from "metabase/public/components/widgets/EmbedModalContent";
 
 import * as Urls from "metabase/lib/urls";
+import MetabaseAnalytics from "metabase/lib/analytics";
 
 import {
   createPublicLink,
@@ -26,6 +33,8 @@ const mapDispatchToProps = {
   mapDispatchToProps,
 )
 export default class DashboardEmbedWidget extends Component {
+  _modal: ?ModalWithTrigger;
+
   render() {
     const {
       className,
@@ -37,22 +46,45 @@ export default class DashboardEmbedWidget extends Component {
       ...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)
+      <ModalWithTrigger
+        ref={m => (this._modal = m)}
+        full
+        triggerElement={
+          <Tooltip tooltip={t`Sharing and embedding`}>
+            <Icon
+              name="share"
+              onClick={() =>
+                MetabaseAnalytics.trackEvent(
+                  "Sharing / Embedding",
+                  "dashboard",
+                  "Sharing Link Clicked",
+                )
+              }
+            />
+          </Tooltip>
         }
-        getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
-      />
+        triggerClasses={cx(className, "text-brand-hover")}
+        className="scroll-y"
+      >
+        <EmbedModalContent
+          {...props}
+          className={className}
+          resource={dashboard}
+          resourceParameters={dashboard && dashboard.parameters}
+          onCreatePublicLink={() => createPublicLink(dashboard)}
+          onDisablePublicLink={() => deletePublicLink(dashboard)}
+          onUpdateEnableEmbedding={enableEmbedding =>
+            updateEnableEmbedding(dashboard, enableEmbedding)
+          }
+          onUpdateEmbeddingParams={embeddingParams =>
+            updateEmbeddingParams(dashboard, embeddingParams)
+          }
+          onClose={() => {
+            this._modal && this._modal.close();
+          }}
+          getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
+        />
+      </ModalWithTrigger>
     );
   }
 }
diff --git a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx b/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
deleted file mode 100644
index 12c237def99..00000000000
--- a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
+++ /dev/null
@@ -1,78 +0,0 @@
-/* @flow */
-
-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 "ttag";
-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 { Parameter } from "metabase/meta/types/Parameter";
-
-type Props = {
-  className?: string,
-
-  resource: EmbeddableResource,
-  resourceType: string,
-  resourceParameters: Parameter[],
-
-  siteUrl: string,
-  secretKey: string,
-  isAdmin: boolean,
-
-  getPublicUrl: (resource: EmbeddableResource, extension: ?string) => string,
-
-  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;
-
-  _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>
-    );
-  }
-}
diff --git a/frontend/src/metabase/query_builder/components/QueryModals.jsx b/frontend/src/metabase/query_builder/components/QueryModals.jsx
index fa75b659972..af251acf892 100644
--- a/frontend/src/metabase/query_builder/components/QueryModals.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModals.jsx
@@ -12,6 +12,7 @@ import EditQuestionInfoModal from "metabase/query_builder/components/view/EditQu
 
 import CollectionMoveModal from "metabase/containers/CollectionMoveModal";
 import ArchiveQuestionModal from "metabase/query_builder/containers/ArchiveQuestionModal";
+import QuestionEmbedWidget from "metabase/query_builder/containers/QuestionEmbedWidget";
 
 import QuestionHistoryModal from "metabase/query_builder/containers/QuestionHistoryModal";
 import { CreateAlertModalContent } from "metabase/query_builder/components/AlertModals";
@@ -118,6 +119,25 @@ export default class QueryModals extends React.Component {
           initialCollectionId={this.props.initialCollectionId}
         />
       </Modal>
+    ) : modal === "save-question-before-embed" ? (
+      <Modal onClose={onCloseModal}>
+        <SaveQuestionModal
+          card={this.props.card}
+          originalCard={this.props.originalCard}
+          tableMetadata={this.props.tableMetadata}
+          saveFn={async card => {
+            await this.props.onSave(card, false);
+            onOpenModal("embed");
+          }}
+          createFn={async card => {
+            await this.props.onCreate(card, false);
+            onOpenModal("embed");
+          }}
+          onClose={onCloseModal}
+          multiStep
+          initialCollectionId={this.props.initialCollectionId}
+        />
+      </Modal>
     ) : modal === "history" ? (
       <Modal onClose={onCloseModal}>
         <QuestionHistoryModal
@@ -157,6 +177,10 @@ export default class QueryModals extends React.Component {
           onSave={card => this.props.onSave(card, false)}
         />
       </Modal>
+    ) : modal === "embed" ? (
+      <Modal full onClose={onCloseModal}>
+        <QuestionEmbedWidget card={this.props.card} onClose={onCloseModal} />
+      </Modal>
     ) : null;
   }
 }
diff --git a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
index 2bf78803d36..387363d15a3 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
@@ -5,6 +5,7 @@ import { t } from "ttag";
 
 import MainPane from "./MainPane";
 import DatabasePane from "./DatabasePane";
+import SchemaPane from "./SchemaPane";
 import TablePane from "./TablePane";
 import FieldPane from "./FieldPane";
 import SegmentPane from "./SegmentPane";
@@ -13,8 +14,9 @@ import MetricPane from "./MetricPane";
 import SidebarContent from "metabase/query_builder/components/SidebarContent";
 
 const PANES = {
-  database: DatabasePane,
-  table: TablePane,
+  database: DatabasePane, // displays either schemas or tables in a database
+  schema: SchemaPane, // displays tables in a schema
+  table: TablePane, // displays fields in a table
   field: FieldPane,
   segment: SegmentPane,
   metric: MetricPane,
diff --git a/frontend/src/metabase/query_builder/components/dataref/DatabasePane.jsx b/frontend/src/metabase/query_builder/components/dataref/DatabasePane.jsx
index fc7c71cdaf9..d1836d83bbb 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DatabasePane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DatabasePane.jsx
@@ -1,36 +1,14 @@
 /* eslint "react/prop-types": "warn" */
 import React from "react";
 import PropTypes from "prop-types";
-import { isQueryable } from "metabase/lib/table";
-import Icon from "metabase/components/Icon";
+import DatabaseSchemasPane from "./DatabaseSchemasPane";
+import DatabaseTablesPane from "./DatabaseTablesPane";
 
-const DatabasePane = ({ database, show, ...props }) => (
-  <div>
-    <div className="ml1 my2 flex align-center justify-between border-bottom pb1">
-      <div className="flex align-center">
-        <Icon name="database" className="text-medium pr1" size={14} />
-        <h3 className="text-wrap">{database.name}</h3>
-      </div>
-      <div className="flex align-center">
-        <Icon name="table2" className="text-light pr1" size={12} />
-        <span className="text-medium">{database.tables.length}</span>
-      </div>
-    </div>
-
-    <ul>
-      {database.tables.filter(isQueryable).map((table, index) => (
-        <li key={table.id}>
-          <a
-            className="flex-full flex p1 text-bold text-brand text-wrap no-decoration bg-medium-hover"
-            onClick={() => show("table", table)}
-          >
-            {table.name}
-          </a>
-        </li>
-      ))}
-    </ul>
-  </div>
-);
+const DatabasePane = props => {
+  const schemas = new Set(props.database.tables.map(t => t.schema));
+  const Component = schemas.size > 1 ? DatabaseSchemasPane : DatabaseTablesPane;
+  return <Component {...props} />;
+};
 
 DatabasePane.propTypes = {
   show: PropTypes.func.isRequired,
diff --git a/frontend/src/metabase/query_builder/components/dataref/DatabaseSchemasPane.jsx b/frontend/src/metabase/query_builder/components/dataref/DatabaseSchemasPane.jsx
new file mode 100644
index 00000000000..f37aded3387
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/dataref/DatabaseSchemasPane.jsx
@@ -0,0 +1,44 @@
+/* eslint "react/prop-types": "warn" */
+import React from "react";
+import PropTypes from "prop-types";
+import Icon from "metabase/components/Icon";
+
+const DatabaseSchemasPane = ({ database, show, ...props }) => {
+  const schemaNames = Array.from(
+    new Set(database.tables.map(t => t.schema)),
+  ).sort((a, b) => a.localeCompare(b));
+  return (
+    <div>
+      <div className="ml1 my2 flex align-center justify-between border-bottom pb1">
+        <div className="flex align-center">
+          <Icon name="database" className="text-medium pr1" size={14} />
+          <h3 className="text-wrap">{database.name}</h3>
+        </div>
+        <div className="flex align-center">
+          <Icon name="folder" className="text-light pr1" size={12} />
+          <span className="text-medium">{schemaNames.length}</span>
+        </div>
+      </div>
+
+      <ul>
+        {schemaNames.map(schema => (
+          <li key={schema}>
+            <a
+              className="flex-full flex p1 text-bold text-brand text-wrap no-decoration bg-medium-hover"
+              onClick={() => show("schema", { database, schema })}
+            >
+              {schema}
+            </a>
+          </li>
+        ))}
+      </ul>
+    </div>
+  );
+};
+
+DatabaseSchemasPane.propTypes = {
+  show: PropTypes.func.isRequired,
+  database: PropTypes.object.isRequired,
+};
+
+export default DatabaseSchemasPane;
diff --git a/frontend/src/metabase/query_builder/components/dataref/DatabaseTablesPane.jsx b/frontend/src/metabase/query_builder/components/dataref/DatabaseTablesPane.jsx
new file mode 100644
index 00000000000..dd85fe35537
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/dataref/DatabaseTablesPane.jsx
@@ -0,0 +1,45 @@
+/* eslint "react/prop-types": "warn" */
+import React from "react";
+import PropTypes from "prop-types";
+import { isQueryable } from "metabase/lib/table";
+import Icon from "metabase/components/Icon";
+
+const DatabaseTablesPane = ({ database, show, ...props }) => {
+  const tables = database.tables
+    .filter(isQueryable)
+    .sort((a, b) => a.name.localeCompare(b.name));
+  return (
+    <div>
+      <div className="ml1 my2 flex align-center justify-between border-bottom pb1">
+        <div className="flex align-center">
+          <Icon name="database" className="text-medium pr1" size={14} />
+          <h3 className="text-wrap">{database.name}</h3>
+        </div>
+        <div className="flex align-center">
+          <Icon name="table2" className="text-light pr1" size={12} />
+          <span className="text-medium">{tables.length}</span>
+        </div>
+      </div>
+
+      <ul>
+        {tables.map(table => (
+          <li key={table.id}>
+            <a
+              className="flex-full flex p1 text-bold text-brand text-wrap no-decoration bg-medium-hover"
+              onClick={() => show("table", table)}
+            >
+              {table.name}
+            </a>
+          </li>
+        ))}
+      </ul>
+    </div>
+  );
+};
+
+DatabaseTablesPane.propTypes = {
+  show: PropTypes.func.isRequired,
+  database: PropTypes.object.isRequired,
+};
+
+export default DatabaseTablesPane;
diff --git a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
index e55e6d5bc31..28c4e666313 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
@@ -12,6 +12,7 @@ const MainPane = ({ databases, show }) => (
     <ul>
       {databases &&
         databases
+          .filter(db => !db.is_saved_questions)
           .filter(db => db.tables && db.tables.length > 0)
           .map(database => (
             <li className="mb2" key={database.id}>
diff --git a/frontend/src/metabase/query_builder/components/dataref/SchemaPane.jsx b/frontend/src/metabase/query_builder/components/dataref/SchemaPane.jsx
new file mode 100644
index 00000000000..6e7306e6c3f
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/dataref/SchemaPane.jsx
@@ -0,0 +1,46 @@
+/* eslint "react/prop-types": "warn" */
+import React from "react";
+import PropTypes from "prop-types";
+import { isQueryable } from "metabase/lib/table";
+import Icon from "metabase/components/Icon";
+
+const SchemaPane = ({ schema: { database, schema }, show, ...props }) => {
+  const tables = database.tables
+    .filter(t => t.schema === schema)
+    .filter(isQueryable)
+    .sort((a, b) => a.name.localeCompare(b.name));
+  return (
+    <div>
+      <div className="ml1 my2 flex align-center justify-between border-bottom pb1">
+        <div className="flex align-center">
+          <Icon name="folder" className="text-medium pr1" size={14} />
+          <h3 className="text-wrap">{schema}</h3>
+        </div>
+        <div className="flex align-center">
+          <Icon name="table2" className="text-light pr1" size={12} />
+          <span className="text-medium">{tables.length}</span>
+        </div>
+      </div>
+
+      <ul>
+        {tables.map(table => (
+          <li key={table.id}>
+            <a
+              className="flex-full flex p1 text-bold text-brand text-wrap no-decoration bg-medium-hover"
+              onClick={() => show("table", table)}
+            >
+              {table.name}
+            </a>
+          </li>
+        ))}
+      </ul>
+    </div>
+  );
+};
+
+SchemaPane.propTypes = {
+  show: PropTypes.func.isRequired,
+  schema: PropTypes.object.isRequired,
+};
+
+export default SchemaPane;
diff --git a/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx b/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx
index 12813fa7611..0e764b44927 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx
@@ -15,7 +15,9 @@ import ViewButton from "./ViewButton";
 
 import QuestionAlertWidget from "./QuestionAlertWidget";
 import QueryDownloadWidget from "metabase/query_builder/components/QueryDownloadWidget";
-import QuestionEmbedWidget from "metabase/query_builder/containers/QuestionEmbedWidget";
+import QuestionEmbedWidget, {
+  QuestionEmbedWidgetTrigger,
+} from "metabase/query_builder/containers/QuestionEmbedWidget";
 
 import { QuestionFilterWidget } from "./QuestionFilters";
 import { QuestionSummarizeWidget } from "./QuestionSummaries";
@@ -166,10 +168,12 @@ const ViewFooter = ({
             />
           ),
           QuestionEmbedWidget.shouldRender({ question, isAdmin }) && (
-            <QuestionEmbedWidget
-              key="embed"
-              className="mx1 hide sm-show"
-              card={question.card()}
+            <QuestionEmbedWidgetTrigger
+              onClick={() =>
+                question.isSaved()
+                  ? onOpenModal("embed")
+                  : onOpenModal("save-question-before-embed")
+              }
             />
           ),
         ]}
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.jsx b/frontend/src/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.jsx
index 35113b9877f..445b8bd9b4a 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.jsx
@@ -5,38 +5,49 @@ import ChartSettings from "metabase/visualizations/components/ChartSettings";
 import visualizations from "metabase/visualizations";
 import SidebarContent from "metabase/query_builder/components/SidebarContent";
 
-const ChartSettingsSidebar = ({
-  question,
-  result,
-  addField,
-  initialChartSetting,
-  onReplaceAllVisualizationSettings,
-  onClose,
-  onOpenChartType,
-  ...props
-}) =>
-  result && (
-    <SidebarContent
-      className="full-height"
-      title={t`${visualizations.get(question.display()).uiName} options`}
-      onDone={onClose}
-      onBack={onOpenChartType}
-    >
-      <ChartSettings
-        question={question}
-        addField={addField}
-        series={[
-          {
-            card: question.card(),
-            data: result.data,
-          },
-        ]}
-        onChange={onReplaceAllVisualizationSettings}
-        onClose={onClose}
-        noPreview
-        initial={initialChartSetting}
-      />
-    </SidebarContent>
-  );
+export default class ChartSettingsSidebar extends React.Component {
+  state = { sidebarPropsOverride: null };
 
-export default ChartSettingsSidebar;
+  setSidebarPropsOverride = sidebarPropsOverride =>
+    this.setState({ sidebarPropsOverride });
+
+  render() {
+    const {
+      question,
+      result,
+      addField,
+      initialChartSetting,
+      onReplaceAllVisualizationSettings,
+      onClose,
+      onOpenChartType,
+    } = this.props;
+    const { sidebarPropsOverride } = this.state;
+    return (
+      result && (
+        <SidebarContent
+          className="full-height"
+          title={t`${visualizations.get(question.display()).uiName} options`}
+          onDone={onClose}
+          onBack={onOpenChartType}
+          {...sidebarPropsOverride}
+        >
+          <ChartSettings
+            question={question}
+            addField={addField}
+            series={[
+              {
+                card: question.card(),
+                data: result.data,
+              },
+            ]}
+            onChange={onReplaceAllVisualizationSettings}
+            onClose={onClose}
+            noPreview
+            initial={initialChartSetting}
+            setSidebarPropsOverride={this.setSidebarPropsOverride}
+          />
+        </SidebarContent>
+      )
+    );
+  }
+}
diff --git a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
index ad8089e107f..ce28888812f 100644
--- a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
+++ b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
@@ -3,10 +3,13 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
 
-import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
+import Icon from "metabase/components/Icon";
+
+import EmbedModalContent from "metabase/public/components/widgets/EmbedModalContent";
 
 import * as Urls from "metabase/lib/urls";
 import MetabaseSettings from "metabase/lib/settings";
+import MetabaseAnalytics from "metabase/lib/analytics";
 
 import { getParameters } from "metabase/meta/Card";
 import {
@@ -39,7 +42,7 @@ export default class QuestionEmbedWidget extends Component {
       ...props
     } = this.props;
     return (
-      <EmbedWidget
+      <EmbedModalContent
         {...props}
         className={className}
         resource={card}
@@ -56,7 +59,6 @@ export default class QuestionEmbedWidget extends Component {
         getPublicUrl={({ public_uuid }, extension) =>
           Urls.publicQuestion(public_uuid, extension)
         }
-        extensions={["csv", "xlsx", "json"]}
       />
     );
   }
@@ -69,9 +71,29 @@ export default class QuestionEmbedWidget extends Component {
     isEmbeddingEnabled = MetabaseSettings.get("embedding"),
   }) {
     return (
-      question.isSaved() &&
-      ((isPublicLinksEnabled && (isAdmin || question.publicUUID())) ||
-        (isEmbeddingEnabled && isAdmin))
+      (isPublicLinksEnabled && (isAdmin || question.publicUUID())) ||
+      (isEmbeddingEnabled && isAdmin)
     );
   }
 }
+
+export function QuestionEmbedWidgetTrigger({
+  onClick,
+}: {
+  onClick: () => void,
+}) {
+  return (
+    <Icon
+      name="share"
+      className="mx1 hide sm-show text-brand-hover cursor-pointer"
+      onClick={() => {
+        MetabaseAnalytics.trackEvent(
+          "Sharing / Embedding",
+          "question",
+          "Sharing Link Clicked",
+        );
+        onClick();
+      }}
+    />
+  );
+}
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index f91efd2d289..a39c8844ec2 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -121,12 +121,6 @@ export const LdapApi = {
   updateSettings: PUT("/api/ldap/settings"),
 };
 
-// adds a flag to google analytics provided segments and metrics
-// we use this when we just want to filter out our own segments/metrics
-function addGoogleAnalyticsFlag(segmentOrMetric) {
-  return { ...segmentOrMetric, googleAnalyics: true };
-}
-
 export const MetabaseApi = {
   db_list: GET("/api/database"),
   db_list_with_tables: GET(
@@ -163,9 +157,24 @@ export const MetabaseApi = {
       // HACK: inject GA metadata that we don't have intergrated on the backend yet
       if (table && table.db && table.db.engine === "googleanalytics") {
         const GA = await getGAMetadata();
-        table.fields = table.fields.map(f => ({ ...f, ...GA.fields[f.name] }));
-        table.metrics.push(...GA.metrics.map(addGoogleAnalyticsFlag));
-        table.segments.push(...GA.segments.map(addGoogleAnalyticsFlag));
+        table.fields = table.fields.map(field => ({
+          ...field,
+          ...GA.fields[field.name],
+        }));
+        table.metrics.push(
+          ...GA.metrics.map(metric => ({
+            ...metric,
+            table_id: table.id,
+            googleAnalyics: true,
+          })),
+        );
+        table.segments.push(
+          ...GA.segments.map(segment => ({
+            ...segment,
+            table_id: table.id,
+            googleAnalyics: true,
+          })),
+        );
       }
 
       if (table && table.fields) {
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
index 93ea6f187ac..fc177309673 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
@@ -136,7 +136,13 @@ class ChartSettings extends Component {
   }
 
   render() {
-    const { question, addField, noPreview, children } = this.props;
+    const {
+      question,
+      addField,
+      noPreview,
+      children,
+      setSidebarPropsOverride,
+    } = this.props;
     const { currentWidget } = this.state;
 
     const settings = this._getSettings();
@@ -208,6 +214,7 @@ class ChartSettings extends Component {
         key={`${widget.id}`}
         {...widget}
         {...extraWidgetProps}
+        setSidebarPropsOverride={setSidebarPropsOverride}
       />
     ));
 
@@ -225,10 +232,19 @@ class ChartSettings extends Component {
       });
     }
 
+    const showSectionPicker =
+      // don't show section tabs for a single section
+      sectionNames.length > 1 &&
+      // hide the section picker if the only widget is column_settings
+      !(
+        visibleWidgets.length === 1 &&
+        visibleWidgets[0].id === "column_settings"
+      );
+
     // default layout with visualization
     return (
       <div>
-        {sectionNames.length > 1 && (
+        {showSectionPicker && (
           <div className="flex flex-no-shrink pl4 pt2 pb1">{sectionPicker}</div>
         )}
         {noPreview ? (
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartNestedSettingColumns.jsx b/frontend/src/metabase/visualizations/components/settings/ChartNestedSettingColumns.jsx
index 0b9b2a3387e..dd77f4624af 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartNestedSettingColumns.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartNestedSettingColumns.jsx
@@ -2,8 +2,6 @@
 
 import React from "react";
 
-import Icon from "metabase/components/Icon";
-
 import ColumnItem from "./ColumnItem";
 
 const displayNameForColumn = column =>
@@ -16,31 +14,9 @@ export default class ChartNestedSettingColumns extends React.Component {
   props: NestedSettingComponentProps;
 
   render() {
-    const {
-      objects,
-      onChangeEditingObject,
-      objectSettingsWidgets,
-      object,
-    } = this.props;
-
+    const { object, objects, onChangeEditingObject } = this.props;
     if (object) {
-      return (
-        <div>
-          {/* only show the back button if we have more than one column */}
-          {objects.length > 1 && (
-            <div
-              className="flex align-center mb2 cursor-pointer"
-              onClick={() => onChangeEditingObject()}
-            >
-              <Icon name="chevronleft" className="text-light" />
-              <span className="ml1 text-bold text-brand text-wrap">
-                {displayNameForColumn(object)}
-              </span>
-            </div>
-          )}
-          {objectSettingsWidgets}
-        </div>
-      );
+      return <ColumnWidgets {...this.props} />;
     } else {
       return (
         <div>
@@ -56,3 +32,32 @@ export default class ChartNestedSettingColumns extends React.Component {
     }
   }
 }
+
+// ColumnWidgets is a component just to hook into mount/unmount
+class ColumnWidgets extends React.Component {
+  componentDidMount() {
+    const {
+      setSidebarPropsOverride,
+      onChangeEditingObject,
+      object,
+    } = this.props;
+
+    if (setSidebarPropsOverride) {
+      setSidebarPropsOverride({
+        title: displayNameForColumn(object),
+        onBack: () => onChangeEditingObject(),
+      });
+    }
+  }
+
+  componentWillUnmount() {
+    const { setSidebarPropsOverride } = this.props;
+    if (setSidebarPropsOverride) {
+      setSidebarPropsOverride(null);
+    }
+  }
+
+  render() {
+    return <div>{this.props.objectSettingsWidgets}</div>;
+  }
+}
diff --git a/frontend/src/metabase/visualizations/lib/apply_axis.js b/frontend/src/metabase/visualizations/lib/apply_axis.js
index 0c72637b2d1..84c15f01f52 100644
--- a/frontend/src/metabase/visualizations/lib/apply_axis.js
+++ b/frontend/src/metabase/visualizations/lib/apply_axis.js
@@ -145,7 +145,9 @@ export function applyChartTimeseriesXAxis(
       const timestampFixed = moment(timestamp)
         .utcOffset(dataOffset)
         .format();
-      const { column, columnSettings } = chart.settings.column(dimensionColumn);
+      const { column, ...columnSettings } = chart.settings.column(
+        dimensionColumn,
+      );
       return formatValue(timestampFixed, {
         ...columnSettings,
         column: { ...column, unit: tickFormatUnit },
diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
index a731f05d418..6c4f42ff6fb 100644
--- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
@@ -33,6 +33,10 @@ function legacyScalarSettingsToFormatOptions(settings) {
     .value();
 }
 
+// used below to determine whether we show compact formatting
+const COMPACT_MAX_WIDTH = 250;
+const COMPACT_MIN_LENGTH = 6;
+
 // Scalar visualization shows a single number
 // Multiseries Scalar is transformed to a Funnel
 export default class Scalar extends Component {
@@ -176,10 +180,10 @@ export default class Scalar extends Component {
       ],
       isDashboard,
       onChangeCardAndRun,
-      gridSize,
       settings,
       visualizationIsClickable,
       onVisualizationClick,
+      width,
     } = this.props;
 
     const columnIndex = this._getColumnIndex(cols, settings);
@@ -198,8 +202,10 @@ export default class Scalar extends Component {
       compact: true,
     });
 
+    // use the compact version of formatting if the component is narrower than
+    // the cutoff and the formatted value is longer than the cutoff
     const displayCompact =
-      fullScalarValue.length > 6 && gridSize && gridSize.width < 4;
+      fullScalarValue.length > COMPACT_MIN_LENGTH && width < COMPACT_MAX_WIDTH;
     const displayValue = displayCompact ? compactScalarValue : fullScalarValue;
 
     const clicked = { value, column };
diff --git a/frontend/test/__support__/sample_dataset_fixture.js b/frontend/test/__support__/sample_dataset_fixture.js
index 1aa3bb34ea8..1bfb5729e55 100644
--- a/frontend/test/__support__/sample_dataset_fixture.js
+++ b/frontend/test/__support__/sample_dataset_fixture.js
@@ -31,14 +31,14 @@ function aliasTablesAndFields(metadata) {
   }
 }
 
-export function createMetadata(updateState) {
+export function createMetadata(updateState = state => state) {
   const stateModified = updateState(chain(state)).value();
   const metadata = getMetadata(stateModified);
   aliasTablesAndFields(metadata);
   return metadata;
 }
 
-export const metadata = createMetadata(state => state);
+export const metadata = createMetadata();
 
 export const SAMPLE_DATASET = metadata.database(SAMPLE_DATASET_ID);
 export const ANOTHER_DATABASE = metadata.database(ANOTHER_DATABASE_ID);
@@ -71,7 +71,7 @@ export function makeMetadata(metadata) {
   // convienence for filling in missing bits
   for (const objects of Object.values(metadata)) {
     for (const [id, object] of Object.entries(objects)) {
-      object.id = parseInt(id);
+      object.id = /^\d+$/.test(id) ? parseInt(id) : id;
       if (!object.name && object.display_name) {
         object.name = object.display_name;
       }
diff --git a/frontend/test/metabase-lib/lib/metadata/Metric.unit.spec.js b/frontend/test/metabase-lib/lib/metadata/Metric.unit.spec.js
new file mode 100644
index 00000000000..e02259b6f60
--- /dev/null
+++ b/frontend/test/metabase-lib/lib/metadata/Metric.unit.spec.js
@@ -0,0 +1,55 @@
+import { metadata, makeMetadata } from "__support__/sample_dataset_fixture";
+
+import Metric from "metabase-lib/lib/metadata/Metric";
+import Table from "metabase-lib/lib/metadata/Table";
+
+describe("Metric", () => {
+  describe("Standard database", () => {
+    const metric = metadata.metric(1);
+
+    it("should be a Metric", () => {
+      expect(metric).toBeInstanceOf(Metric);
+    });
+    it("should have a Table", () => {
+      expect(metric.table).toBeInstanceOf(Table);
+    });
+
+    describe("displayName", () => {
+      it("should return the metric name", () => {
+        expect(metric.displayName()).toBe("Total Order Value");
+      });
+    });
+    describe("aggregationClause", () => {
+      it('should return ["metric", 1]', () => {
+        expect(metric.aggregationClause()).toEqual(["metric", 1]);
+      });
+    });
+    describe("columnName", () => {
+      it("should return the underlying metric definition name", () => {
+        expect(metric.columnName()).toBe("sum");
+      });
+    });
+  });
+
+  describe("Google Analytics database", () => {
+    const metadata = makeMetadata({
+      metrics: { "ga:users": { name: "Users" } },
+    });
+    const metric = metadata.metric("ga:users");
+    describe("displayName", () => {
+      it("should return the metric name", () => {
+        expect(metric.displayName()).toBe("Users");
+      });
+    });
+    describe("aggregationClause", () => {
+      it('should return ["metric", "ga:users]', () => {
+        expect(metric.aggregationClause()).toEqual(["metric", "ga:users"]);
+      });
+    });
+    describe("columnName", () => {
+      it("should return the metric id", () => {
+        expect(metric.columnName()).toBe("ga:users");
+      });
+    });
+  });
+});
diff --git a/frontend/test/metabase/public/public.e2e.spec.js b/frontend/test/metabase/public/public.e2e.spec.js
index 13fade43535..4e834b1c5d6 100644
--- a/frontend/test/metabase/public/public.e2e.spec.js
+++ b/frontend/test/metabase/public/public.e2e.spec.js
@@ -64,8 +64,7 @@ import PreviewPane from "metabase/public/components/widgets/PreviewPane";
 import CopyWidget from "metabase/components/CopyWidget";
 import ListSearchField from "metabase/components/ListSearchField";
 import * as Urls from "metabase/lib/urls";
-import QuestionEmbedWidget from "metabase/query_builder/containers/QuestionEmbedWidget";
-import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
+import { QuestionEmbedWidgetTrigger } from "metabase/query_builder/containers/QuestionEmbedWidget";
 
 import { CardApi, DashboardApi, SettingsApi } from "metabase/services";
 
@@ -242,7 +241,7 @@ describe("public/embedded", () => {
       await delay(500);
 
       // open sharing panel
-      click(app.find(QuestionEmbedWidget).find(EmbedWidget));
+      click(app.find(QuestionEmbedWidgetTrigger));
 
       // "Embed this question in an application"
       click(
diff --git a/frontend/test/metabase/query_builder/components/dataref/DataReference.unit.spec.js b/frontend/test/metabase/query_builder/components/dataref/DataReference.unit.spec.js
new file mode 100644
index 00000000000..2ec3adf48ff
--- /dev/null
+++ b/frontend/test/metabase/query_builder/components/dataref/DataReference.unit.spec.js
@@ -0,0 +1,60 @@
+import React from "react";
+import { render, cleanup, fireEvent } from "@testing-library/react";
+import DataReference from "metabase/query_builder/components/dataref/DataReference";
+
+const databases = [
+  {
+    name: "db1",
+    id: 1,
+    tables: [
+      { name: "t1", id: 1, schema: "s1" },
+      { name: "t2", id: 2, schema: "s2" },
+      { name: "t3", id: 3, schema: "s2", visibility_type: "hidden" },
+    ],
+  },
+  { name: "db2", id: 2, tables: [{ name: "t4", id: 4 }] },
+  {
+    name: "saved questions",
+    is_saved_questions: true,
+    tables: [{ name: "t5", id: 5 }],
+  },
+  { name: "empty", tables: [] },
+];
+
+describe("DatabasePane", () => {
+  afterEach(cleanup);
+
+  it("should show databases except empty databases and saved questions db", () => {
+    const { getByText, queryByText } = render(
+      <DataReference databases={databases} />,
+    );
+    getByText("db1");
+    getByText("db2");
+    expect(queryByText("saved questions")).toBe(null);
+    expect(queryByText("empty")).toBe(null);
+  });
+
+  it("should show tables in db without multple schemas", () => {
+    const { getByText } = render(<DataReference databases={databases} />);
+    fireEvent.click(getByText("db2"));
+    getByText("t4");
+  });
+
+  it("should show schemas in db with multple schemas", () => {
+    const { getByText } = render(<DataReference databases={databases} />);
+    fireEvent.click(getByText("db1"));
+    getByText("s1");
+    getByText("s2");
+  });
+
+  it("should only show visible tables", () => {
+    const { getByText, queryByText } = render(
+      <DataReference databases={databases} />,
+    );
+    fireEvent.click(getByText("db1"));
+    fireEvent.click(getByText("s2"));
+    getByText("1"); // table count with filtered tables
+    getByText("t2");
+    expect(queryByText("t3")).toBe(null);
+  });
+});
diff --git a/frontend/test/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.unit.spec.js b/frontend/test/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.unit.spec.js
new file mode 100644
index 00000000000..3d82cea57b5
--- /dev/null
+++ b/frontend/test/metabase/query_builder/components/view/sidebars/ChartSettingsSidebar.unit.spec.js
@@ -0,0 +1,27 @@
+import React from "react";
+import "@testing-library/jest-dom/extend-expect";
+import { render, fireEvent } from "@testing-library/react";
+
+import { SAMPLE_DATASET } from "__support__/sample_dataset_fixture";
+
+import ChartSettingsSidebar from "metabase/query_builder/components/view/sidebars/ChartSettingsSidebar";
+
+describe("ChartSettingsSidebar", () => {
+  it("should hide title and section picker when viewing column settings", () => {
+    const data = {
+      rows: [["bar"]],
+      cols: [{ base_type: "type/Text", name: "foo", display_name: "foo" }],
+    };
+    const { container, getByText, queryByText } = render(
+      <ChartSettingsSidebar
+        question={SAMPLE_DATASET.question()}
+        result={{ data }}
+      />,
+    );
+    getByText("Table options");
+    getByText("Conditional Formatting");
+    fireEvent.click(container.querySelector(".Icon-gear"));
+    expect(queryByText("Table options")).toBe(null);
+    expect(queryByText("Conditional Formatting")).toBe(null);
+  });
+});
diff --git a/frontend/test/metabase/visualizations/components/ChartSettings.unit.spec.js b/frontend/test/metabase/visualizations/components/ChartSettings.unit.spec.js
index aeff73ffdca..974ee2147c4 100644
--- a/frontend/test/metabase/visualizations/components/ChartSettings.unit.spec.js
+++ b/frontend/test/metabase/visualizations/components/ChartSettings.unit.spec.js
@@ -103,4 +103,43 @@ describe("ChartSettings", () => {
     expect(getByText("Widget1", { exact: false })).toBeInTheDocument();
     expect(queryByText("Widget2", { exact: false })).toBe(null);
   });
+
+  it("should show the section picker if there are multiple sections", () => {
+    const { getByText } = render(
+      <ChartSettings
+        {...DEFAULT_PROPS}
+        widgets={[
+          widget({ title: "Widget1", section: "Foo" }),
+          widget({ title: "Widget2", section: "Bar" }),
+        ]}
+      />,
+    );
+    expect(getByText("Foo")).toBeInTheDocument();
+  });
+
+  it("should not show the section picker if there's only one section", () => {
+    const { queryByText } = render(
+      <ChartSettings
+        {...DEFAULT_PROPS}
+        widgets={[
+          widget({ title: "Something", section: "Foo" }),
+          widget({ title: "Other Thing", section: "Foo" }),
+        ]}
+      />,
+    );
+    expect(queryByText("Foo")).toBe(null);
+  });
+
+  it("should not show the section picker if showing a column setting", () => {
+    const { queryByText } = render(
+      <ChartSettings
+        {...DEFAULT_PROPS}
+        widgets={[
+          widget({ title: "Something", section: "Foo", id: "column_settings" }),
+          widget({ title: "Other Thing", section: "Bar", id: "other_thing" }),
+        ]}
+      />,
+    );
+    expect(queryByText("Foo")).toBe(null);
+  });
 });
diff --git a/frontend/test/metabase/visualizations/components/LineAreaBarRenderer.unit.spec.js b/frontend/test/metabase/visualizations/components/LineAreaBarRenderer.unit.spec.js
index 32d139f9344..f3984bf50ae 100644
--- a/frontend/test/metabase/visualizations/components/LineAreaBarRenderer.unit.spec.js
+++ b/frontend/test/metabase/visualizations/components/LineAreaBarRenderer.unit.spec.js
@@ -159,7 +159,10 @@ describe("LineAreaBarRenderer", () => {
 
     // column settings are cached based on name.
     // we need something unique to not conflict with other tests.
-    const dateColumn = DateTimeColumn({ unit: "week", name: "uniqueName123" });
+    const dateColumn = DateTimeColumn({
+      unit: "week",
+      name: Math.random().toString(36),
+    });
 
     const cols = [dateColumn, NumberColumn()];
     const chartType = "line";
@@ -180,6 +183,43 @@ describe("LineAreaBarRenderer", () => {
     expect(ticks).toEqual(["January, 2020", "February, 2020", "March, 2020"]);
   });
 
+  it("should use column settings for tick formatting and tooltips", () => {
+    const rows = [["2016-01-01", 1], ["2016-02-01", 2]];
+
+    // column settings are cached based on name.
+    // we need something unique to not conflict with other tests.
+    const columnName = Math.random().toString(36);
+    const dateColumn = DateTimeColumn({ unit: "month", name: columnName });
+
+    const cols = [dateColumn, NumberColumn()];
+    const chartType = "line";
+    const column_settings = {
+      [`["name","${columnName}"]`]: {
+        date_style: "M/D/YYYY",
+        date_separator: "-",
+      },
+    };
+    const card = {
+      display: chartType,
+      visualization_settings: { column_settings },
+    };
+    const series = [{ data: { cols, rows }, card }];
+    const settings = getComputedSettingsForSeries(series);
+    const onHoverChange = jest.fn();
+
+    const props = { chartType, series, settings, onHoverChange };
+    lineAreaBarRenderer(element, props);
+
+    dispatchUIEvent(qs(".dot"), "mousemove");
+
+    const hover = onHoverChange.mock.calls[0][0];
+    const [formattedWeek] = getFormattedTooltips(hover, settings);
+    expect(formattedWeek).toEqual("1-2016");
+
+    const ticks = qsa(".axis.x .tick text").map(e => e.textContent);
+    expect(ticks).toEqual(["1-2016", "2-2016"]);
+  });
+
   describe("should render correctly a compound line graph", () => {
     const rowsOfNonemptyCard = [[2015, 1], [2016, 2], [2017, 3]];
 
diff --git a/frontend/test/metabase/visualizations/visualizations/Scalar.unit.spec.js b/frontend/test/metabase/visualizations/visualizations/Scalar.unit.spec.js
index 465d61d36bb..2788162f28f 100644
--- a/frontend/test/metabase/visualizations/visualizations/Scalar.unit.spec.js
+++ b/frontend/test/metabase/visualizations/visualizations/Scalar.unit.spec.js
@@ -36,7 +36,7 @@ describe("MetricForm", () => {
         series={series(12345)}
         settings={settings}
         visualizationIsClickable={() => false}
-        gridSize={{ width: 3 }}
+        width={230}
       />,
     );
     getByText("12,345"); // with compact formatting, we'd have 1
@@ -48,7 +48,7 @@ describe("MetricForm", () => {
         series={series(12345.6)}
         settings={settings}
         visualizationIsClickable={() => false}
-        gridSize={{ width: 3 }}
+        width={230}
       />,
     );
     getByText("12.3k");
diff --git a/modules/drivers/redshift/project.clj b/modules/drivers/redshift/project.clj
index c8594d55579..d6f1a5a92f4 100644
--- a/modules/drivers/redshift/project.clj
+++ b/modules/drivers/redshift/project.clj
@@ -1,4 +1,4 @@
-(defproject metabase/redshift-driver "1.0.0-SNAPSHOT-1.2.32.1056"
+(defproject metabase/redshift-driver "1.0.0-SNAPSHOT-1.2.36.1060"
   :min-lein-version "2.5.0"
 
   :repositories
@@ -6,7 +6,7 @@
 
 
   :dependencies
-  [[com.amazon.redshift/redshift-jdbc42-no-awssdk "1.2.32.1056"]]
+  [[com.amazon.redshift/redshift-jdbc42-no-awssdk "1.2.36.1060"]]
 
   :profiles
   {:provided
diff --git a/modules/drivers/redshift/resources/metabase-plugin.yaml b/modules/drivers/redshift/resources/metabase-plugin.yaml
index 2b6dba5ea21..ba10bf4f604 100644
--- a/modules/drivers/redshift/resources/metabase-plugin.yaml
+++ b/modules/drivers/redshift/resources/metabase-plugin.yaml
@@ -1,6 +1,6 @@
 info:
   name: Metabase Redshift Driver
-  version: 1.0.0-SNAPSHOT-1.2.32.1056
+  version: 1.0.0-SNAPSHOT-1.2.36.1060
   description: Allows Metabase to connect to Redshift databases.
 driver:
   name: redshift
diff --git a/src/metabase/pulse/render/style.clj b/src/metabase/pulse/render/style.clj
index 307aa7f5e5f..58a51263c61 100644
--- a/src/metabase/pulse/render/style.clj
+++ b/src/metabase/pulse/render/style.clj
@@ -47,10 +47,22 @@
   "~25% gray."
   "#394340")
 
-(def ^:const color-row-border
+(def ^:const color-text-medium
+  "Color for medium text."
+  "#74838f")
+
+(def ^:const color-text-dark
+  "Color for dark text."
+  "#2E353B")
+
+(def ^:const color-header-row-border
   "Used as color for the bottom border of table headers for charts with `:table` vizualization."
   "#EDF0F1")
 
+(def ^:const color-body-row-border
+  "Used as color for the bottom border of table body rows for charts with `:table` vizualization."
+  "#F0F0F04D")
+
 ;; don't try to improve the code and make this a plain variable, in EE it's customizable which is why it's a function.
 ;; Too much of a hassle to have it be a fn in one version of the code an a constant in another
 (defn primary-color
diff --git a/src/metabase/pulse/render/table.clj b/src/metabase/pulse/render/table.clj
index c9ae4a7666e..61495931f64 100644
--- a/src/metabase/pulse/render/table.clj
+++ b/src/metabase/pulse/render/table.clj
@@ -15,23 +15,25 @@
 (defn- bar-th-style []
   (merge
    (style/font-style)
-   {:font-size :14.22px
+   {:font-size :12.5px
     :font-weight     700
-    :color           style/color-gray-4
-    :border-bottom   (str "1px solid " style/color-row-border)
+    :color           style/color-text-medium
+    :border-bottom   (str "1px solid " style/color-header-row-border)
     :padding-top     :20px
     :padding-bottom  :5px}))
 
 (defn- bar-td-style []
   (merge
    (style/font-style)
-   {:font-size      :14.22px
-    :font-weight    400
+   {:font-size      :12.5px
+    :font-weight    700
     :text-align     :left
+    :color          style/color-text-dark
+    :border-bottom  (str "1px solid " style/color-body-row-border)
+    :height         :36px
+    :width          :106px
     :padding-right  :0.5em
-    :padding-left   :0.5em
-    :padding-top    :4px
-    :padding-bottom :4px}))
+    :padding-left   :0.5em}))
 
 (defn- bar-th-style-numeric []
   (merge (style/font-style) (bar-th-style) {:text-align :right}))
diff --git a/webpack.config.js b/webpack.config.js
index 7e68b055140..f8d71c2d3c8 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -61,7 +61,7 @@ const config = (module.exports = {
     path: BUILD_PATH + "/app/dist",
     // NOTE: the filename on disk won't include "?[chunkhash]" but the URL in index.html generated by HtmlWebpackPlugin will:
     filename: "[name].bundle.js?[hash]",
-    publicPath: "/app/dist/",
+    publicPath: "app/dist/",
   },
 
   module: {
@@ -85,7 +85,7 @@ const config = (module.exports = {
       },
       {
         test: /\.(eot|woff2?|ttf|svg|png)$/,
-        use: [{ loader: "file-loader" }],
+        use: [{ loader: "file-loader", options: { publicPath: "" } }],
       },
       {
         test: /\.css$/,
-- 
GitLab