Skip to content
Snippets Groups Projects
Unverified Commit 2a59a826 authored by Cam Saul's avatar Cam Saul Committed by GitHub
Browse files

Merge pull request #11923 from metabase/merge-release-0.34.x

Weekly merge of release-0.34.x into master
parents 020c965d f345af4c
No related branches found
No related tags found
No related merge requests found
Showing
with 210 additions and 204 deletions
......@@ -67,6 +67,7 @@ executors:
environment:
POSTGRES_USER: metabase_test
POSTGRES_DB: metabase_test
POSTGRES_HOST_AUTH_METHOD: trust
mysql-5-7:
parameters:
......
......@@ -847,22 +847,30 @@ export class AggregationDimension extends Dimension {
}
column(extra = {}) {
const aggregation = this.aggregation();
const { special_type, ...column } = super.column();
return {
...column,
// don't pass through `special_type` when aggregating these types
...(!UNAGGREGATED_SPECIAL_TYPES.has(special_type) && { special_type }),
base_type: aggregation ? aggregation.baseType() : TYPE.Float,
...super.column(),
source: "aggregation",
...extra,
};
}
field() {
// FIXME: it isn't really correct to return the unaggregated field. return a fake Field object?
const dimension = this.aggregation().dimension();
return dimension ? dimension.field() : super.field();
const aggregation = this.aggregation();
if (!aggregation) {
return super.field();
}
const dimension = aggregation.dimension();
const field = dimension && dimension.field();
const { special_type } = field || {};
return new Field({
name: aggregation.columnName(),
display_name: aggregation.displayName(),
base_type: aggregation.baseType(),
// don't pass through `special_type` when aggregating these types
...(!UNAGGREGATED_SPECIAL_TYPES.has(special_type) && { special_type }),
query: this._query,
metadata: this._metadata,
});
}
/**
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import { t } from "ttag";
import _ from "underscore";
import cx from "classnames";
import { getIn } from "icepick";
import { connect } from "react-redux";
import Visualization from "metabase/visualizations/components/Visualization";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import Icon from "metabase/components/Icon";
import Tooltip from "metabase/components/Tooltip";
import CheckBox from "metabase/components/CheckBox";
import MetabaseAnalytics from "metabase/lib/analytics";
import * as Q_DEPRECATED from "metabase/lib/query";
import { color } from "metabase/lib/colors";
import { getVisualizationRaw } from "metabase/visualizations";
import Questions from "metabase/entities/questions";
import _ from "underscore";
import cx from "classnames";
import { getIn } from "icepick";
import { getVisualizationRaw } from "metabase/visualizations";
function getQueryColumns(card, databases) {
const dbId = card.dataset_query.database;
......@@ -33,6 +36,15 @@ function getQueryColumns(card, databases) {
return Q_DEPRECATED.getQueryColumns(table, query);
}
// TODO: rework this so we don't have to load all cards up front
@connect(
state => ({
cards: Questions.selectors.getList(state, { entityQuery: { f: "all" } }),
}),
{
fetchCards: () => Questions.actions.fetchList({ f: "all" }),
},
)
export default class AddSeriesModal extends Component {
constructor(props, context) {
super(props, context);
......
import React, { Component } from "react";
import React from "react";
import PropTypes from "prop-types";
import { t } from "ttag";
import MetabaseAnalytics from "metabase/lib/analytics";
import AddToDashboard from "metabase/questions/containers/AddToDashboard";
export default class AddToDashSelectQuestionModal extends Component {
constructor(props, context) {
super(props, context);
import ModalContent from "metabase/components/ModalContent";
import QuestionPicker from "metabase/containers/QuestionPicker";
this.state = {
error: null,
};
}
import MetabaseAnalytics from "metabase/lib/analytics";
export default class AddToDashSelectQuestionModal extends React.Component {
static propTypes = {
dashboard: PropTypes.object.isRequired,
cards: PropTypes.array,
fetchCards: PropTypes.func.isRequired,
addCardToDashboard: PropTypes.func.isRequired,
onEditingChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
async componentDidMount() {
try {
await this.props.fetchCards();
} catch (error) {
console.error(error);
this.setState({ error });
}
}
onAdd = cardId => {
handleAdd = cardId => {
this.props.addCardToDashboard({
dashId: this.props.dashboard.id,
cardId: cardId,
......@@ -44,6 +26,13 @@ export default class AddToDashSelectQuestionModal extends Component {
};
render() {
return <AddToDashboard onAdd={this.onAdd} onClose={this.props.onClose} />;
return (
<ModalContent
title={t`Pick a question to add`}
onClose={this.props.onClose}
>
<QuestionPicker onChange={this.handleAdd} />
</ModalContent>
);
}
}
......@@ -24,11 +24,7 @@ import type {
QueryParams,
} from "metabase/meta/types";
import type {
Card,
CardId,
VisualizationSettings,
} from "metabase/meta/types/Card";
import type { CardId, VisualizationSettings } from "metabase/meta/types/Card";
import type {
DashboardWithCards,
DashboardId,
......@@ -47,7 +43,6 @@ type Props = {
dashboardId: DashboardId,
dashboard: DashboardWithCards,
cards: Card[],
revisions: { [key: string]: Revision[] },
isAdmin: boolean,
......@@ -64,7 +59,6 @@ type Props = {
addCardToDashboard: ({ dashId: DashCardId, cardId: CardId }) => void,
addTextDashCardToDashboard: ({ dashId: DashCardId }) => void,
archiveDashboard: (dashboardId: DashboardId) => void,
fetchCards: (filterMode?: string) => void,
fetchDashboard: (dashboardId: DashboardId, queryParams: ?QueryParams) => void,
saveDashboardAndCards: () => Promise<void>,
setDashboardAttributes: ({ [attribute: string]: any }) => void,
......@@ -133,12 +127,10 @@ export default class Dashboard extends Component {
isEditingParameter: PropTypes.bool.isRequired,
dashboard: PropTypes.object,
cards: PropTypes.array,
parameters: PropTypes.array,
addCardToDashboard: PropTypes.func.isRequired,
archiveDashboard: PropTypes.func.isRequired,
fetchCards: PropTypes.func.isRequired,
fetchDashboard: PropTypes.func.isRequired,
saveDashboardAndCards: PropTypes.func.isRequired,
setDashboardAttributes: PropTypes.func.isRequired,
......@@ -181,7 +173,6 @@ export default class Dashboard extends Component {
const {
addCardOnLoad,
fetchDashboard,
fetchCards,
addCardToDashboard,
setErrorPage,
location,
......@@ -190,8 +181,6 @@ export default class Dashboard extends Component {
try {
await fetchDashboard(dashboardId, location.query);
if (addCardOnLoad != null) {
// we have to load our cards before we can add one
await fetchCards();
this.setEditing(this.props.dashboard);
addCardToDashboard({ dashId: dashboardId, cardId: addCardOnLoad });
}
......
......@@ -47,7 +47,6 @@ export default class DashboardGrid extends Component {
isEditingParameter: PropTypes.bool.isRequired,
dashboard: PropTypes.object.isRequired,
parameterValues: PropTypes.object.isRequired,
cards: PropTypes.array,
setDashCardAttributes: PropTypes.func.isRequired,
removeCardFromDashboard: PropTypes.func.isRequired,
......@@ -161,10 +160,8 @@ export default class DashboardGrid extends Component {
<AddSeriesModal
dashcard={this.state.addSeriesModalDashCard}
dashboard={this.props.dashboard}
cards={this.props.cards}
dashcardData={this.props.dashcardData}
databases={this.props.databases}
fetchCards={this.props.fetchCards}
fetchCardData={this.props.fetchCardData}
fetchDatabaseMetadata={this.props.fetchDatabaseMetadata}
removeCardFromDashboard={this.props.removeCardFromDashboard}
......
......@@ -23,7 +23,7 @@ import MetabaseSettings from "metabase/lib/settings";
import cx from "classnames";
import type { LocationDescriptor, QueryParams } from "metabase/meta/types";
import type { Card, CardId } from "metabase/meta/types/Card";
import type { CardId } from "metabase/meta/types/Card";
import type {
Parameter,
ParameterId,
......@@ -40,7 +40,6 @@ type Props = {
location: LocationDescriptor,
dashboard: DashboardWithCards,
cards: Card[],
isAdmin: boolean,
isEditable: boolean,
......@@ -56,7 +55,6 @@ type Props = {
addCardToDashboard: ({ dashId: DashCardId, cardId: CardId }) => void,
addTextDashCardToDashboard: ({ dashId: DashCardId }) => void,
archiveDashboard: (dashboardId: DashboardId) => void,
fetchCards: (filterMode?: string) => void,
fetchDashboard: (dashboardId: DashboardId, queryParams: ?QueryParams) => void,
saveDashboardAndCards: () => Promise<void>,
setDashboardAttribute: (attribute: string, value: any) => void,
......@@ -96,7 +94,6 @@ export default class DashboardHeader extends Component {
addCardToDashboard: PropTypes.func.isRequired,
addTextDashCardToDashboard: PropTypes.func.isRequired,
archiveDashboard: PropTypes.func.isRequired,
fetchCards: PropTypes.func.isRequired,
fetchDashboard: PropTypes.func.isRequired,
saveDashboardAndCards: PropTypes.func.isRequired,
setDashboardAttribute: PropTypes.func.isRequired,
......@@ -238,8 +235,6 @@ export default class DashboardHeader extends Component {
>
<AddToDashSelectQuestionModal
dashboard={dashboard}
cards={this.props.cards}
fetchCards={this.props.fetchCards}
addCardToDashboard={this.props.addCardToDashboard}
onEditingChange={this.props.onEditingChange}
onClose={() => this.refs.addQuestionModal.toggle()}
......
......@@ -7,8 +7,6 @@ import Button from "metabase/components/Button";
import ModalContent from "metabase/components/ModalContent";
export default class RemoveFromDashboardModal extends Component {
state = { deleteCard: false };
static propTypes = {
dashcard: PropTypes.object.isRequired,
dashboard: PropTypes.object.isRequired,
......@@ -21,10 +19,6 @@ export default class RemoveFromDashboardModal extends Component {
dashId: this.props.dashboard.id,
dashcardId: this.props.dashcard.id,
});
if (this.state.deleteCard) {
// this.props.dispatch(deleteCard(this.props.dashcard.card_id))
// this.props.dispatch(markCardForDeletion(this.props.dashcard.card_id))
}
this.props.onClose();
MetabaseAnalytics.trackEvent("Dashboard", "Remove Card");
......
......@@ -16,7 +16,6 @@ import {
getIsEditingParameter,
getIsDirty,
getDashboardComplete,
getCardList,
getCardData,
getSlowCards,
getEditingParameter,
......@@ -41,7 +40,6 @@ const mapStateToProps = (state, props) => {
isEditingParameter: getIsEditingParameter(state, props),
isDirty: getIsDirty(state, props),
dashboard: getDashboardComplete(state, props),
cards: getCardList(state, props),
dashcardData: getCardData(state, props),
slowCards: getSlowCards(state, props),
databases: getDatabases(state, props),
......
......@@ -2,7 +2,6 @@
import { assoc, dissoc, assocIn, getIn, chain } from "icepick";
import _ from "underscore";
import moment from "moment";
import {
handleActions,
......@@ -15,6 +14,7 @@ import { defer } from "metabase/lib/promise";
import { normalize, schema } from "normalizr";
import Dashboards from "metabase/entities/dashboards";
import Questions from "metabase/entities/questions";
import {
createParameter,
......@@ -29,7 +29,7 @@ import type {
DashCard,
DashCardId,
} from "metabase/meta/types/Dashboard";
import type { Card, CardId } from "metabase/meta/types/Card";
import type { CardId } from "metabase/meta/types/Card";
import Utils from "metabase/lib/utils";
import { getPositionForNewDashCard } from "metabase/lib/dashboard_grid";
......@@ -59,7 +59,6 @@ const DATASET_SLOW_TIMEOUT = 15 * 1000;
// normalizr schemas
const dashcard = new schema.Entity("dashcard");
const card = new schema.Entity("card");
const dashboard = new schema.Entity("dashboard", {
ordered_cards: [dashcard],
});
......@@ -70,9 +69,6 @@ export const INITIALIZE = "metabase/dashboard/INITIALIZE";
export const SET_EDITING_DASHBOARD = "metabase/dashboard/SET_EDITING_DASHBOARD";
export const FETCH_CARDS = "metabase/dashboard/FETCH_CARDS";
export const DELETE_CARD = "metabase/dashboard/DELETE_CARD";
// NOTE: this is used in metabase/redux/metadata but can't be imported directly due to circular reference
export const FETCH_DASHBOARD = "metabase/dashboard/FETCH_DASHBOARD";
export const SAVE_DASHBOARD_AND_CARDS =
......@@ -141,58 +137,39 @@ export const markNewCardSeen = createAction(MARK_NEW_CARD_SEEN);
export const setDashboardAttributes = createAction(SET_DASHBOARD_ATTRIBUTES);
export const setDashCardAttributes = createAction(SET_DASHCARD_ATTRIBUTES);
// TODO: consolidate with questions reducer
export const fetchCards = createThunkAction(FETCH_CARDS, function(
filterMode = "all",
) {
return async function(dispatch, getState) {
const cards = await CardApi.list({ f: filterMode });
for (const c of cards) {
c.updated_at = moment(c.updated_at);
}
return normalize(cards, [card]);
};
});
export const deleteCard = createThunkAction(DELETE_CARD, function(cardId) {
return async function(dispatch, getState) {
await CardApi.delete({ cardId });
return cardId;
};
});
export const addCardToDashboard = function({
export const addCardToDashboard = ({
dashId,
cardId,
}: {
dashId: DashCardId,
cardId: CardId,
}) {
return function(dispatch, getState) {
const { dashboards, dashcards, cards } = getState().dashboard;
const dashboard: DashboardWithCards = dashboards[dashId];
const existingCards: Array<DashCard> = dashboard.ordered_cards
.map(id => dashcards[id])
.filter(dc => !dc.isRemoved);
const card: Card = cards[cardId];
const dashcard: DashCard = {
id: Math.random(), // temporary id
dashboard_id: dashId,
card_id: card.id,
card: card,
series: [],
...getPositionForNewDashCard(existingCards),
parameter_mappings: [],
visualization_settings: {},
};
dispatch(createAction(ADD_CARD_TO_DASH)(dashcard));
dispatch(fetchCardData(card, dashcard, { reload: true, clear: true }));
// guard in case card was filtered
if (card.dataset_query && card.dataset_query.database) {
dispatch(fetchDatabaseMetadata(card.dataset_query.database));
}
}) => async (dispatch, getState) => {
await dispatch(Questions.actions.fetch({ id: cardId }));
const card = Questions.selectors.getObject(getState(), {
entityId: cardId,
});
const { dashboards, dashcards } = getState().dashboard;
const dashboard: DashboardWithCards = dashboards[dashId];
const existingCards: Array<DashCard> = dashboard.ordered_cards
.map(id => dashcards[id])
.filter(dc => !dc.isRemoved);
const dashcard: DashCard = {
id: Math.random(), // temporary id
dashboard_id: dashId,
card_id: card.id,
card: card,
series: [],
...getPositionForNewDashCard(existingCards),
parameter_mappings: [],
visualization_settings: {},
};
dispatch(createAction(ADD_CARD_TO_DASH)(dashcard));
dispatch(fetchCardData(card, dashcard, { reload: true, clear: true }));
// guard in case card was filtered
if (card.dataset_query && card.dataset_query.database) {
dispatch(fetchDatabaseMetadata(card.dataset_query.database));
}
};
export const addDashCardToDashboard = function({
......@@ -961,24 +938,6 @@ const isEditing = handleActions(
false,
);
// TODO: consolidate with questions reducer
const cards = handleActions(
{
[FETCH_CARDS]: {
next: (state, { payload }) => ({ ...payload.entities.card }),
},
},
{},
);
const cardList = handleActions(
{
[FETCH_CARDS]: { next: (state, { payload }) => payload.result },
[DELETE_CARD]: { next: (state, { payload }) => state },
},
null,
);
export function syncParametersAndEmbeddingParams(before, after) {
if (after.parameters && before.embedding_params) {
return Object.keys(before.embedding_params).reduce((memo, embedSlug) => {
......@@ -1217,8 +1176,6 @@ const loadingDashCards = handleActions(
export default combineReducers({
dashboardId,
isEditing,
cards,
cardList,
dashboards,
dashcards,
editingParameterId,
......
......@@ -37,12 +37,10 @@ export type MappingsByParameter = {
export const getDashboardId = state => state.dashboard.dashboardId;
export const getIsEditing = state => state.dashboard.isEditing;
export const getCards = state => state.dashboard.cards;
export const getDashboards = state => state.dashboard.dashboards;
export const getDashcards = state => state.dashboard.dashcards;
export const getCardData = state => state.dashboard.dashcardData;
export const getSlowCards = state => state.dashboard.slowCards;
export const getCardIdList = state => state.dashboard.cardList;
export const getParameterValues = state => state.dashboard.parameterValues;
export const getLoadingStartTime = state =>
state.dashboard.loadingDashCards.startTime;
......@@ -80,11 +78,6 @@ export const getIsDirty = createSelector(
),
);
export const getCardList = createSelector(
[getCardIdList, getCards],
(cardIdList, cards) => cardIdList && cardIdList.map(id => cards[id]),
);
export const getEditingParameterId = state =>
state.dashboard.editingParameterId;
......@@ -121,10 +114,10 @@ export const getMappingsByParameter = createSelector(
}
let mappingsByParameter: MappingsByParameter = {};
const mappings: Array<AugmentedParameterMapping> = [];
const mappings: AugmentedParameterMapping[] = [];
const countsByParameter = {};
for (const dashcard of dashboard.ordered_cards) {
const cards: Array<Card> = [dashcard.card].concat(dashcard.series);
const cards: Card[] = [dashcard.card].concat(dashcard.series);
for (const mapping: ParameterMapping of dashcard.parameter_mappings ||
[]) {
const card = _.findWhere(cards, { id: mapping.card_id });
......
......@@ -120,7 +120,7 @@ function getDefaultNumberOptions(options) {
return defaults;
}
const PRECISION_NUMBER_FORMATTER = d3.format(".2r");
const PRECISION_NUMBER_FORMATTER = d3.format(".2f");
const FIXED_NUMBER_FORMATTER = d3.format(",.f");
const DECIMAL_DEGREES_FORMATTER = d3.format(".08f");
const DECIMAL_DEGREES_FORMATTER_COMPACT = d3.format(".02f");
......@@ -261,23 +261,32 @@ function formatNumberScientific(
}
}
const DISPLAY_COMPACT_DECIMALS_CUTOFF = 1000;
export const COMPACT_CURRENCY_OPTIONS = {
// Currencies vary in how many decimals they display, so this is probably
// wrong in some cases. Intl.NumberFormat has some of that data built-in, but
// I couldn't figure out how to use it here.
digits: 2,
currency_style: "symbol",
};
function formatNumberCompact(value: number, options: FormattingOptions) {
if (options.number_style === "percent") {
return formatNumberCompactWithoutOptions(value * 100) + "%";
}
if (options.number_style === "currency") {
try {
const { value: currency } = numberFormatterForOptions({
const nf = numberFormatterForOptions({
...options,
currency_style: "symbol",
})
.formatToParts(value)
.find(p => p.type === "currency");
...COMPACT_CURRENCY_OPTIONS,
});
// this special case ensures the "~" comes before the currency
if (value !== 0 && value >= -0.01 && value <= 0.01) {
return `~${currency}0`;
if (Math.abs(value) < DISPLAY_COMPACT_DECIMALS_CUTOFF) {
return nf.format(value);
}
const { value: currency } = nf
.formatToParts(value)
.find(p => p.type === "currency");
return currency + formatNumberCompactWithoutOptions(value);
} catch (e) {
// Intl.NumberFormat failed, so we fall back to a non-currency number
......@@ -299,10 +308,7 @@ function formatNumberCompactWithoutOptions(value: number) {
if (value === 0) {
// 0 => 0
return "0";
} else if (value >= -0.01 && value <= 0.01) {
// 0.01 => ~0
return "~ 0";
} else if (value > -1 && value < 1) {
} else if (Math.abs(value) < DISPLAY_COMPACT_DECIMALS_CUTOFF) {
// 0.1 => 0.1
return PRECISION_NUMBER_FORMATTER(value).replace(/\.?0+$/, "");
} else {
......
import React, { Component } from "react";
import { t } from "ttag";
import ModalContent from "metabase/components/ModalContent";
import QuestionPicker from "metabase/containers/QuestionPicker";
export default class AddToDashboard extends Component {
render() {
return (
<ModalContent
title={t`Pick a question to add`}
onClose={this.props.onClose}
>
<QuestionPicker onChange={this.props.onAdd} />
</ModalContent>
);
}
}
......@@ -5,6 +5,7 @@ import _ from "underscore";
import { color } from "metabase/lib/colors";
import { clipPathReference } from "metabase/lib/dom";
import { COMPACT_CURRENCY_OPTIONS } from "metabase/lib/formatting";
import { adjustYAxisTicksIfNeeded } from "./apply_axis";
import { isHistogramBar } from "./renderer_utils";
......@@ -271,6 +272,31 @@ function onRenderValueLabels(chart, formatYValue, [data]) {
return { x, y, showLabelBelow };
});
const formattingSetting = chart.settings["graph.label_value_formatting"];
let compact;
if (formattingSetting === "compact") {
compact = true;
} else if (formattingSetting === "full") {
compact = false;
} else {
// for "auto" we use compact if it shortens avg label length by >3 chars
const getAvgLength = compact => {
const options = {
compact,
// We include compact currency options here for both compact and
// non-compact formatting. This prevents auto's logic from depending on
// those settings.
...COMPACT_CURRENCY_OPTIONS,
// We need this to ensure the settings are used. Otherwise, a cached
// _numberFormatter would take precedence.
_numberFormatter: undefined,
};
const lengths = data.map(d => formatYValue(d.y, options).length);
return lengths.reduce((sum, l) => sum + l, 0) / lengths.length;
};
compact = getAvgLength(true) < getAvgLength(false) - 3;
}
// use the chart body so things line up properly
const parent = chart.svg().select(".chart-body");
......@@ -322,7 +348,7 @@ function onRenderValueLabels(chart, formatYValue, [data]) {
.append("text")
.attr("class", klass)
.attr("text-anchor", "middle")
.text(({ y }) => formatYValue(y, { compact: true })),
.text(({ y }) => formatYValue(y, { compact })),
);
};
......
......@@ -346,6 +346,24 @@ export const GRAPH_DISPLAY_VALUES_SETTINGS = {
default: "fit",
readDependencies: ["graph.show_values"],
},
"graph.label_value_formatting": {
section: t`Display`,
title: t`Value formatting`,
widget: "radio",
getHidden: (series, vizSettings) =>
series.length > 1 ||
vizSettings["graph.show_values"] !== true ||
vizSettings["stackable.stack_type"] === "normalized",
props: {
options: [
{ name: t`Auto`, value: "auto" },
{ name: t`Compact`, value: "compact" },
{ name: t`Full`, value: "full" },
],
},
default: "auto",
readDependencies: ["graph.show_values"],
},
};
export const GRAPH_COLORS_SETTINGS = {
......
......@@ -39,7 +39,12 @@ export default class Smart extends React.Component {
],
settings,
) => [
_.find(cols, col => col.name === settings["scalar.field"]) || cols[1],
// try and find a selected field setting
cols.find(col => col.name === settings["scalar.field"]) ||
// fall back to the second column
cols[1] ||
// but if there's only one column use that
cols[0],
],
}),
"scalar.switch_positive_negative": {
......
......@@ -509,17 +509,21 @@ describe("Dimension", () => {
});
});
function aggregation(agg) {
const query = new StructuredQuery(ORDERS.question(), {
type: "query",
database: SAMPLE_DATASET.id,
query: {
"source-table": ORDERS.id,
aggregation: [agg],
},
});
return Dimension.parseMBQL(["aggregation", 0], metadata, query);
}
describe("column()", () => {
function sumOf(column) {
const query = new StructuredQuery(ORDERS.question(), {
type: "query",
database: SAMPLE_DATASET.id,
query: {
"source-table": ORDERS.id,
aggregation: [["sum", ["field-id", column.id]]],
},
});
return Dimension.parseMBQL(["aggregation", 0], metadata, query);
return aggregation(["sum", ["field-id", column.id]]);
}
it("should clear unaggregated special types", () => {
......@@ -534,6 +538,29 @@ describe("Dimension", () => {
expect(special_type).toBe("type/Currency");
});
});
describe("field()", () => {
it("should return a float field for sum of order total", () => {
const { base_type } = aggregation([
"sum",
["field-id", ORDERS.TOTAL.id],
]).field();
expect(base_type).toBe("type/Float");
});
it("should return an int field for count distinct of product category", () => {
const { base_type } = aggregation([
"distinct",
["field-id", PRODUCTS.CATEGORY.id],
]).field();
expect(base_type).toBe("type/Integer");
});
it("should return an int field for count", () => {
const { base_type } = aggregation(["count"]).field();
expect(base_type).toBe("type/Integer");
});
});
});
});
});
......@@ -37,14 +37,14 @@ describe("formatting", () => {
it("shouldn't display small numbers as 0", () => {
expect(formatNumber(0.1, { compact: true })).toEqual("0.1");
expect(formatNumber(-0.1, { compact: true })).toEqual("-0.1");
expect(formatNumber(0.01, { compact: true })).toEqual("~ 0");
expect(formatNumber(-0.01, { compact: true })).toEqual("~ 0");
expect(formatNumber(0.01, { compact: true })).toEqual("0.01");
expect(formatNumber(-0.01, { compact: true })).toEqual("-0.01");
});
it("should round up and down", () => {
expect(formatNumber(1.01, { compact: true })).toEqual("1");
expect(formatNumber(-1.01, { compact: true })).toEqual("-1");
expect(formatNumber(1.9, { compact: true })).toEqual("2");
expect(formatNumber(-1.9, { compact: true })).toEqual("-2");
expect(formatNumber(1.01, { compact: true })).toEqual("1.01");
expect(formatNumber(-1.01, { compact: true })).toEqual("-1.01");
expect(formatNumber(1.9, { compact: true })).toEqual("1.9");
expect(formatNumber(-1.9, { compact: true })).toEqual("-1.9");
});
it("should format large numbers with metric units", () => {
expect(formatNumber(1, { compact: true })).toEqual("1");
......@@ -55,12 +55,12 @@ describe("formatting", () => {
const options = { compact: true, number_style: "percent" };
expect(formatNumber(0, options)).toEqual("0%");
expect(formatNumber(0.001, options)).toEqual("0.1%");
expect(formatNumber(0.0001, options)).toEqual("~ 0%");
expect(formatNumber(0.0001, options)).toEqual("0.01%");
expect(formatNumber(0.001234, options)).toEqual("0.12%");
expect(formatNumber(0.1, options)).toEqual("10%");
expect(formatNumber(0.1234, options)).toEqual("12%");
expect(formatNumber(0.019, options)).toEqual("2%");
expect(formatNumber(0.021, options)).toEqual("2%");
expect(formatNumber(0.1234, options)).toEqual("12.34%");
expect(formatNumber(0.019, options)).toEqual("1.9%");
expect(formatNumber(0.021, options)).toEqual("2.1%");
expect(formatNumber(11.11, options)).toEqual("1.1k%");
expect(formatNumber(-0.22, options)).toEqual("-22%");
});
......@@ -79,9 +79,11 @@ describe("formatting", () => {
number_style: "currency",
currency: "USD",
};
expect(formatNumber(0, options)).toEqual("$0");
expect(formatNumber(0.001, options)).toEqual("~$0");
expect(formatNumber(7.24, options)).toEqual("$7");
expect(formatNumber(0, options)).toEqual("$0.00");
expect(formatNumber(0.001, options)).toEqual("$0.00");
expect(formatNumber(7.24, options)).toEqual("$7.24");
expect(formatNumber(7.249, options)).toEqual("$7.25");
expect(formatNumber(724.9, options)).toEqual("$724.90");
expect(formatNumber(1234.56, options)).toEqual("$1.2k");
expect(formatNumber(1234567.89, options)).toEqual("$1.2M");
expect(formatNumber(-1234567.89, options)).toEqual("$-1.2M");
......
......@@ -4,6 +4,7 @@ import { render, cleanup } from "@testing-library/react";
import { NumberColumn, DateTimeColumn } from "../__support__/visualizations";
import Visualization from "metabase/visualizations/components/Visualization";
import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
const series = ({ rows, insights }) => {
const cols = [
......@@ -93,4 +94,10 @@ describe("SmartScalar", () => {
);
getAllByText("8,000%");
});
it("shouldn't throw an error getting settings for single-column data", () => {
const card = { display: "smartscalar", visualization_settings: {} };
const data = { cols: [NumberColumn({ name: "Count" })], rows: [[100]] };
expect(() => getSettingsWidgetsForSeries([{ card, data }])).not.toThrow();
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment