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