You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2023/06/28 18:51:46 UTC
[superset] branch master updated: feat: Implement currencies formatter for saved metrics (#24517)
This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 83ff4cd86a feat: Implement currencies formatter for saved metrics (#24517)
83ff4cd86a is described below
commit 83ff4cd86a4931fc8eda83aeb3d8d3c92d773202
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Wed Jun 28 20:51:40 2023 +0200
feat: Implement currencies formatter for saved metrics (#24517)
---
superset-frontend/package-lock.json | 9 +-
.../superset-ui-chart-controls/src/fixtures.ts | 2 +
.../superset-ui-chart-controls/src/types.ts | 2 +
.../test/utils/columnChoices.test.tsx | 1 +
.../test/utils/defineSavedMetrics.test.tsx | 1 +
.../src/currency-format/CurrencyFormatter.ts | 79 +++++++++++
.../src/{types => currency-format}/index.ts | 10 +-
.../packages/superset-ui-core/src/index.ts | 1 +
.../superset-ui-core/src/query/types/Datasource.ts | 8 ++
.../superset-ui-core/src/query/types/Metric.ts | 1 +
.../packages/superset-ui-core/src/types/index.ts | 5 +
.../test/currency-format/CurrencyFormatter.test.ts | 158 +++++++++++++++++++++
.../src/BigNumber/BigNumberTotal/transformProps.ts | 25 +++-
.../BigNumberWithTrendline/transformProps.ts | 18 ++-
.../plugin-chart-echarts/src/BigNumber/types.ts | 4 +-
.../src/Funnel/transformProps.ts | 14 +-
.../src/Gauge/transformProps.ts | 15 +-
.../plugin-chart-echarts/src/Pie/transformProps.ts | 15 +-
.../src/Timeseries/transformProps.ts | 78 +++++++++-
.../src/Timeseries/transformers.ts | 4 +-
.../src/Treemap/transformProps.ts | 17 ++-
.../plugin-chart-echarts/src/utils/forecast.ts | 4 +-
.../plugin-chart-echarts/src/utils/series.ts | 3 +-
.../src/utils/valueFormatter.ts | 63 ++++++++
.../test/BigNumber/transformProps.test.ts | 25 ++++
.../src/PivotTableChart.tsx | 31 +++-
.../src/plugin/transformProps.ts | 3 +-
.../plugins/plugin-chart-pivot-table/src/types.ts | 2 +
.../test/plugin/buildQuery.test.ts | 1 +
.../test/plugin/transformProps.test.ts | 1 +
.../plugins/plugin-chart-table/package.json | 42 +++---
.../plugin-chart-table/src/transformProps.ts | 8 +-
.../plugins/plugin-chart-table/src/types.ts | 7 +-
.../plugin-chart-table/src/utils/formatValue.ts | 1 -
.../plugin-chart-table/src/utils/isEqualColumns.ts | 1 +
.../plugin-chart-table/test/TableChart.test.tsx | 21 +++
.../plugins/plugin-chart-table/test/testData.ts | 11 ++
.../src/components/Datasource/DatasourceEditor.jsx | 70 +++++++++
.../Datasource/DatasourceEditor.test.jsx | 86 ++++++++++-
.../src/components/Datasource/DatasourceModal.tsx | 32 ++++-
superset-frontend/src/dashboard/constants.ts | 1 +
.../src/explore/actions/datasourcesActions.test.ts | 2 +
.../src/explore/actions/hydrateExplore.ts | 1 +
.../src/explore/controlUtils/controlUtils.test.tsx | 1 +
...etControlValuesCompatibleWithDatasource.test.ts | 1 +
superset-frontend/src/explore/fixtures.tsx | 2 +
superset-frontend/src/features/datasets/types.ts | 3 +
.../src/utils/getDatasourceUid.test.ts | 1 +
superset/config.py | 2 +
superset/connectors/base/models.py | 21 +++
superset/connectors/sqla/models.py | 1 +
superset/connectors/sqla/views.py | 1 +
superset/dashboards/schemas.py | 1 +
superset/datasets/api.py | 2 +
superset/datasets/schemas.py | 2 +
superset/db_engine_specs/base.py | 1 +
superset/explore/schemas.py | 1 +
..._90139bf715e4_add_currency_column_to_metrics.py | 42 ++++++
superset/views/base.py | 1 +
tests/integration_tests/datasets/commands_tests.py | 3 +
tests/unit_tests/datasets/commands/export_test.py | 1 +
61 files changed, 888 insertions(+), 82 deletions(-)
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index d2493fdb3a..d926b16fec 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -61705,12 +61705,14 @@
"regenerator-runtime": "^0.13.7",
"xss": "^1.0.10"
},
- "devDependencies": {
- "@testing-library/react": "^11.2.0"
- },
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
+ "@testing-library/dom": "^7.29.4",
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.0",
+ "@testing-library/react-hooks": "^5.0.3",
+ "@testing-library/user-event": "^12.7.0",
"@types/classnames": "*",
"@types/react": "*",
"react": "^16.13.1",
@@ -77467,7 +77469,6 @@
"version": "file:plugins/plugin-chart-table",
"requires": {
"@react-icons/all-files": "^4.1.0",
- "@testing-library/react": "^11.2.0",
"@types/d3-array": "^2.9.0",
"@types/enzyme": "^3.10.5",
"@types/react-table": "^7.0.29",
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts
index 1e5152b2bd..cc0b678f6d 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts
@@ -21,6 +21,7 @@ import { Dataset } from './types';
export const TestDataset: Dataset = {
column_formats: {},
+ currency_formats: {},
columns: [
{
advanced_data_type: undefined,
@@ -123,6 +124,7 @@ export const TestDataset: Dataset = {
certification_details: null,
certified_by: null,
d3format: null,
+ currency: null,
description: null,
expression: 'COUNT(*)',
id: 7,
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index 8ba428d7bc..dadd78cdd0 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -21,6 +21,7 @@ import React, { ReactElement, ReactNode, ReactText } from 'react';
import type {
AdhocColumn,
Column,
+ Currency,
DatasourceType,
JsonObject,
JsonValue,
@@ -68,6 +69,7 @@ export interface Dataset {
columns: ColumnMeta[];
metrics: Metric[];
column_formats: Record<string, string>;
+ currency_formats: Record<string, Currency>;
verbose_map: Record<string, string>;
main_dttm_col: string;
// eg. ['["ds", true]', 'ds [asc]']
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
index e27fa95120..aaaccda95d 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
@@ -43,6 +43,7 @@ describe('columnChoices()', () => {
],
verbose_map: {},
column_formats: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' },
+ currency_formats: {},
datasource_name: 'my_datasource',
description: 'this is my datasource',
}),
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
index 765412d592..f7ae98520c 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
@@ -40,6 +40,7 @@ describe('defineSavedMetrics', () => {
columns: [],
verbose_map: {},
column_formats: {},
+ currency_formats: {},
datasource_name: 'my_datasource',
description: 'this is my datasource',
};
diff --git a/superset-frontend/packages/superset-ui-core/src/currency-format/CurrencyFormatter.ts b/superset-frontend/packages/superset-ui-core/src/currency-format/CurrencyFormatter.ts
new file mode 100644
index 0000000000..7c082abd34
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/currency-format/CurrencyFormatter.ts
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ExtensibleFunction } from '../models';
+import { getNumberFormatter, NumberFormats } from '../number-format';
+import { Currency } from '../query';
+
+interface CurrencyFormatterConfig {
+ d3Format?: string;
+ currency: Currency;
+ locale?: string;
+}
+
+interface CurrencyFormatter {
+ (value: number | null | undefined): string;
+}
+
+export const getCurrencySymbol = (currency: Currency) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: currency.symbol,
+ })
+ .formatToParts(1)
+ .find(x => x.type === 'currency')?.value;
+
+class CurrencyFormatter extends ExtensibleFunction {
+ d3Format: string;
+
+ locale: string;
+
+ currency: Currency;
+
+ constructor(config: CurrencyFormatterConfig) {
+ super((value: number) => this.format(value));
+ this.d3Format = config.d3Format || NumberFormats.SMART_NUMBER;
+ this.currency = config.currency;
+ this.locale = config.locale || 'en-US';
+ }
+
+ hasValidCurrency() {
+ return Boolean(this.currency?.symbol);
+ }
+
+ getNormalizedD3Format() {
+ return this.d3Format.replace(/\$|%/g, '');
+ }
+
+ format(value: number) {
+ const formattedValue = getNumberFormatter(this.getNormalizedD3Format())(
+ value,
+ );
+ if (!this.hasValidCurrency()) {
+ return formattedValue as string;
+ }
+
+ if (this.currency.symbolPosition === 'prefix') {
+ return `${getCurrencySymbol(this.currency)} ${formattedValue}`;
+ }
+ return `${formattedValue} ${getCurrencySymbol(this.currency)}`;
+ }
+}
+
+export default CurrencyFormatter;
diff --git a/superset-frontend/packages/superset-ui-core/src/types/index.ts b/superset-frontend/packages/superset-ui-core/src/currency-format/index.ts
similarity index 84%
copy from superset-frontend/packages/superset-ui-core/src/types/index.ts
copy to superset-frontend/packages/superset-ui-core/src/currency-format/index.ts
index a1c527afd6..c7fa5a0388 100644
--- a/superset-frontend/packages/superset-ui-core/src/types/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/currency-format/index.ts
@@ -1,4 +1,4 @@
-/**
+/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -16,10 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-export * from '../query/types';
-export type Maybe<T> = T | null;
-
-export type Optional<T> = T | undefined;
-
-export type ValueOf<T> = T[keyof T];
+export { default as CurrencyFormatter } from './CurrencyFormatter';
+export * from './CurrencyFormatter';
diff --git a/superset-frontend/packages/superset-ui-core/src/index.ts b/superset-frontend/packages/superset-ui-core/src/index.ts
index 5ee5acbce4..ea7a4efde7 100644
--- a/superset-frontend/packages/superset-ui-core/src/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/index.ts
@@ -36,3 +36,4 @@ export * from './components';
export * from './math-expression';
export * from './ui-overrides';
export * from './hooks';
+export * from './currency-format';
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
index 9639a000d0..ab5ff950cc 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
@@ -27,6 +27,11 @@ export enum DatasourceType {
SavedQuery = 'saved_query',
}
+export interface Currency {
+ symbol: string;
+ symbolPosition: string;
+}
+
/**
* Datasource metadata.
*/
@@ -41,6 +46,9 @@ export interface Datasource {
columnFormats?: {
[key: string]: string;
};
+ currencyFormats?: {
+ [key: string]: Currency;
+ };
verboseMap?: {
[key: string]: string;
};
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts
index c0f770f904..ac6523bedb 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts
@@ -65,6 +65,7 @@ export interface Metric {
certification_details?: Maybe<string>;
certified_by?: Maybe<string>;
d3format?: Maybe<string>;
+ currency?: Maybe<string>;
description?: Maybe<string>;
is_certified?: boolean;
verbose_name?: string;
diff --git a/superset-frontend/packages/superset-ui-core/src/types/index.ts b/superset-frontend/packages/superset-ui-core/src/types/index.ts
index a1c527afd6..cb6e6dcfcb 100644
--- a/superset-frontend/packages/superset-ui-core/src/types/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/types/index.ts
@@ -16,6 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { NumberFormatter } from '../number-format';
+import { CurrencyFormatter } from '../currency-format';
+
export * from '../query/types';
export type Maybe<T> = T | null;
@@ -23,3 +26,5 @@ export type Maybe<T> = T | null;
export type Optional<T> = T | undefined;
export type ValueOf<T> = T[keyof T];
+
+export type ValueFormatter = NumberFormatter | CurrencyFormatter;
diff --git a/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
new file mode 100644
index 0000000000..5172e3b0ed
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ CurrencyFormatter,
+ getCurrencySymbol,
+ NumberFormats,
+} from '@superset-ui/core';
+
+it('getCurrencySymbol', () => {
+ expect(
+ getCurrencySymbol({ symbol: 'PLN', symbolPosition: 'prefix' }),
+ ).toEqual('PLN');
+ expect(
+ getCurrencySymbol({ symbol: 'USD', symbolPosition: 'prefix' }),
+ ).toEqual('$');
+
+ expect(() =>
+ getCurrencySymbol({ symbol: 'INVALID_CODE', symbolPosition: 'prefix' }),
+ ).toThrow(RangeError);
+});
+
+it('CurrencyFormatter object fields', () => {
+ const defaultCurrencyFormatter = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ });
+ expect(defaultCurrencyFormatter.d3Format).toEqual(NumberFormats.SMART_NUMBER);
+ expect(defaultCurrencyFormatter.locale).toEqual('en-US');
+ expect(defaultCurrencyFormatter.currency).toEqual({
+ symbol: 'USD',
+ symbolPosition: 'prefix',
+ });
+
+ const currencyFormatter = new CurrencyFormatter({
+ currency: { symbol: 'PLN', symbolPosition: 'suffix' },
+ locale: 'pl-PL',
+ d3Format: ',.1f',
+ });
+ expect(currencyFormatter.d3Format).toEqual(',.1f');
+ expect(currencyFormatter.locale).toEqual('pl-PL');
+ expect(currencyFormatter.currency).toEqual({
+ symbol: 'PLN',
+ symbolPosition: 'suffix',
+ });
+});
+
+it('CurrencyFormatter:hasValidCurrency', () => {
+ const currencyFormatter = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ });
+ expect(currencyFormatter.hasValidCurrency()).toBe(true);
+
+ const currencyFormatterWithoutPosition = new CurrencyFormatter({
+ // @ts-ignore
+ currency: { symbol: 'USD' },
+ });
+ expect(currencyFormatterWithoutPosition.hasValidCurrency()).toBe(true);
+
+ const currencyFormatterWithoutSymbol = new CurrencyFormatter({
+ // @ts-ignore
+ currency: { symbolPosition: 'prefix' },
+ });
+ expect(currencyFormatterWithoutSymbol.hasValidCurrency()).toBe(false);
+
+ // @ts-ignore
+ const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
+ expect(currencyFormatterWithoutCurrency.hasValidCurrency()).toBe(false);
+});
+
+it('CurrencyFormatter:getNormalizedD3Format', () => {
+ const currencyFormatter = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ });
+ expect(currencyFormatter.getNormalizedD3Format()).toEqual(
+ currencyFormatter.d3Format,
+ );
+
+ const currencyFormatter2 = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ d3Format: ',.1f',
+ });
+ expect(currencyFormatter2.getNormalizedD3Format()).toEqual(
+ currencyFormatter2.d3Format,
+ );
+
+ const currencyFormatter3 = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ d3Format: '$,.1f',
+ });
+ expect(currencyFormatter3.getNormalizedD3Format()).toEqual(',.1f');
+
+ const currencyFormatter4 = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ d3Format: ',.1%',
+ });
+ expect(currencyFormatter4.getNormalizedD3Format()).toEqual(',.1');
+});
+
+it('CurrencyFormatter:format', () => {
+ const VALUE = 56100057;
+ const currencyFormatterWithPrefix = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ });
+
+ expect(currencyFormatterWithPrefix(VALUE)).toEqual(
+ currencyFormatterWithPrefix.format(VALUE),
+ );
+ expect(currencyFormatterWithPrefix(VALUE)).toEqual('$ 56.1M');
+
+ const currencyFormatterWithSuffix = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'suffix' },
+ });
+ expect(currencyFormatterWithSuffix(VALUE)).toEqual('56.1M $');
+
+ const currencyFormatterWithoutPosition = new CurrencyFormatter({
+ // @ts-ignore
+ currency: { symbol: 'USD' },
+ });
+ expect(currencyFormatterWithoutPosition(VALUE)).toEqual('56.1M $');
+
+ // @ts-ignore
+ const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
+ expect(currencyFormatterWithoutCurrency(VALUE)).toEqual('56.1M');
+
+ const currencyFormatterWithCustomD3 = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ d3Format: ',.1f',
+ });
+ expect(currencyFormatterWithCustomD3(VALUE)).toEqual('$ 56,100,057.0');
+
+ const currencyFormatterWithPercentD3 = new CurrencyFormatter({
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ d3Format: ',.1f%',
+ });
+ expect(currencyFormatterWithPercentD3(VALUE)).toEqual('$ 56,100,057.0');
+
+ const currencyFormatterWithCurrencyD3 = new CurrencyFormatter({
+ currency: { symbol: 'PLN', symbolPosition: 'suffix' },
+ d3Format: '$,.1f',
+ });
+ expect(currencyFormatterWithCurrencyD3(VALUE)).toEqual('56,100,057.0 PLN');
+});
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
index 8624e5bc54..5486030f46 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
@@ -19,9 +19,9 @@
import {
ColorFormatters,
getColorFormatters,
+ Metric,
} from '@superset-ui/chart-controls';
import {
- getNumberFormatter,
GenericDataType,
getMetricLabel,
extractTimegrain,
@@ -30,12 +30,20 @@ import {
import { BigNumberTotalChartProps, BigNumberVizProps } from '../types';
import { getDateFormatter, parseMetricValue } from '../utils';
import { Refs } from '../../types';
+import { getValueFormatter } from '../../utils/valueFormatter';
export default function transformProps(
chartProps: BigNumberTotalChartProps,
): BigNumberVizProps {
- const { width, height, queriesData, formData, rawFormData, hooks } =
- chartProps;
+ const {
+ width,
+ height,
+ queriesData,
+ formData,
+ rawFormData,
+ hooks,
+ datasource: { currencyFormats = {}, columnFormats = {} },
+ } = chartProps;
const {
headerFontSize,
metric = 'value',
@@ -54,7 +62,7 @@ export default function transformProps(
const bigNumber =
data.length === 0 ? null : parseMetricValue(data[0][metricName]);
- let metricEntry;
+ let metricEntry: Metric | undefined;
if (chartProps.datasource?.metrics) {
metricEntry = chartProps.datasource.metrics.find(
metricItem => metricItem.metric_name === metric,
@@ -67,12 +75,19 @@ export default function transformProps(
metricEntry?.d3format,
);
+ const numberFormatter = getValueFormatter(
+ metric,
+ currencyFormats,
+ columnFormats,
+ yAxisFormat,
+ );
+
const headerFormatter =
coltypes[0] === GenericDataType.TEMPORAL ||
coltypes[0] === GenericDataType.STRING ||
forceTimestampFormatting
? formatTime
- : getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
+ : numberFormatter;
const { onContextMenu } = hooks;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
index bd4553479e..c05a427f31 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
@@ -24,9 +24,10 @@ import {
getMetricLabel,
t,
smartDateVerboseFormatter,
- NumberFormatter,
TimeFormatter,
getXAxisLabel,
+ Metric,
+ ValueFormatter,
} from '@superset-ui/core';
import { EChartsCoreOption, graphic } from 'echarts';
import {
@@ -38,11 +39,12 @@ import {
import { getDateFormatter, parseMetricValue } from '../utils';
import { getDefaultTooltip } from '../../utils/tooltip';
import { Refs } from '../../types';
+import { getValueFormatter } from '../../utils/valueFormatter';
const defaultNumberFormatter = getNumberFormatter();
export function renderTooltipFactory(
formatDate: TimeFormatter = smartDateVerboseFormatter,
- formatValue: NumberFormatter | TimeFormatter = defaultNumberFormatter,
+ formatValue: ValueFormatter | TimeFormatter = defaultNumberFormatter,
) {
return function renderTooltip(params: { data: TimeSeriesDatum }[]) {
return `
@@ -73,6 +75,7 @@ export default function transformProps(
theme,
hooks,
inContextMenu,
+ datasource: { currencyFormats = {}, columnFormats = {} },
} = chartProps;
const {
colorPicker,
@@ -159,7 +162,7 @@ export default function transformProps(
className = 'negative';
}
- let metricEntry;
+ let metricEntry: Metric | undefined;
if (chartProps.datasource?.metrics) {
metricEntry = chartProps.datasource.metrics.find(
metricEntry => metricEntry.metric_name === metric,
@@ -172,12 +175,19 @@ export default function transformProps(
metricEntry?.d3format,
);
+ const numberFormatter = getValueFormatter(
+ metric,
+ currencyFormats,
+ columnFormats,
+ yAxisFormat,
+ );
+
const headerFormatter =
metricColtype === GenericDataType.TEMPORAL ||
metricColtype === GenericDataType.STRING ||
forceTimestampFormatting
? formatTime
- : getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
+ : numberFormatter;
if (trendLineData && timeRangeFixed && fromDatetime) {
const toDatetimeOrToday = toDatetime ?? Date.now();
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
index c517fcc0b9..2081460ad1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
@@ -22,10 +22,10 @@ import {
ChartDataResponseResult,
ContextMenuFilters,
DataRecordValue,
- NumberFormatter,
QueryFormData,
QueryFormMetric,
TimeFormatter,
+ ValueFormatter,
} from '@superset-ui/core';
import { ColorFormatters } from '@superset-ui/chart-controls';
import { BaseChartProps, Refs } from '../types';
@@ -73,7 +73,7 @@ export type BigNumberVizProps = {
height: number;
bigNumber?: DataRecordValue;
bigNumberFallback?: TimeSeriesDatum;
- headerFormatter: NumberFormatter | TimeFormatter;
+ headerFormatter: ValueFormatter | TimeFormatter;
formatTime?: TimeFormatter;
headerFontSize: number;
kickerFontSize?: number;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
index ac2f650e32..796aa36e40 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
@@ -22,7 +22,7 @@ import {
getMetricLabel,
getNumberFormatter,
NumberFormats,
- NumberFormatter,
+ ValueFormatter,
getColumnLabel,
} from '@superset-ui/core';
import { CallbackDataParams } from 'echarts/types/src/util/types';
@@ -45,6 +45,7 @@ import { defaultGrid } from '../defaults';
import { OpacityEnum, DEFAULT_LEGEND_FORM_DATA } from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
+import { getValueFormatter } from '../utils/valueFormatter';
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
@@ -56,7 +57,7 @@ export function formatFunnelLabel({
}: {
params: Pick<CallbackDataParams, 'name' | 'value' | 'percent'>;
labelType: EchartsFunnelLabelTypeType;
- numberFormatter: NumberFormatter;
+ numberFormatter: ValueFormatter;
sanitizeName?: boolean;
}): string {
const { name: rawName = '', value, percent } = params;
@@ -94,6 +95,7 @@ export default function transformProps(
theme,
inContextMenu,
emitCrossFilters,
+ datasource,
} = chartProps;
const data: DataRecord[] = queriesData[0].data || [];
const coltypeMapping = getColtypesMapping(queriesData[0]);
@@ -118,6 +120,7 @@ export default function transformProps(
...DEFAULT_FUNNEL_FORM_DATA,
...formData,
};
+ const { currencyFormats = {}, columnFormats = {} } = datasource;
const refs: Refs = {};
const metricLabel = getMetricLabel(metric);
const groupbyLabels = groupby.map(getColumnLabel);
@@ -139,7 +142,12 @@ export default function transformProps(
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
- const numberFormatter = getNumberFormatter(numberFormat);
+ const numberFormatter = getValueFormatter(
+ metric,
+ currencyFormats,
+ columnFormats,
+ numberFormat,
+ );
const transformedData: FunnelSeriesOption[] = data.map(datum => {
const name = extractGroupbyLabel({
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
index 27e6c9f197..ffbb746bf0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
@@ -21,7 +21,6 @@ import {
CategoricalColorNamespace,
CategoricalColorScale,
DataRecord,
- getNumberFormatter,
getMetricLabel,
getColumnLabel,
} from '@superset-ui/core';
@@ -47,6 +46,7 @@ import { OpacityEnum } from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
import { getColtypesMapping } from '../utils/series';
+import { getValueFormatter } from '../utils/valueFormatter';
const setIntervalBoundsAndColors = (
intervals: string,
@@ -105,7 +105,11 @@ export default function transformProps(
} = chartProps;
const gaugeSeriesOptions = defaultGaugeSeriesOption(theme);
- const { verboseMap = {} } = datasource;
+ const {
+ verboseMap = {},
+ currencyFormats = {},
+ columnFormats = {},
+ } = datasource;
const {
groupby,
metric,
@@ -132,7 +136,12 @@ export default function transformProps(
const refs: Refs = {};
const data = (queriesData[0]?.data || []) as DataRecord[];
const coltypeMapping = getColtypesMapping(queriesData[0]);
- const numberFormatter = getNumberFormatter(numberFormat);
+ const numberFormatter = getValueFormatter(
+ metric,
+ currencyFormats,
+ columnFormats,
+ numberFormat,
+ );
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const axisLineWidth = calculateAxisLineWidth(data, fontSize, overlap);
const groupbyLabels = groupby.map(getColumnLabel);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
index 7d30917b18..7ea9cc1ffb 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
@@ -23,8 +23,8 @@ import {
getNumberFormatter,
getTimeFormatter,
NumberFormats,
- NumberFormatter,
t,
+ ValueFormatter,
} from '@superset-ui/core';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import { EChartsCoreOption, PieSeriesOption } from 'echarts';
@@ -47,6 +47,7 @@ import { defaultGrid } from '../defaults';
import { convertInteger } from '../utils/convertInteger';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
+import { getValueFormatter } from '../utils/valueFormatter';
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
@@ -58,7 +59,7 @@ export function formatPieLabel({
}: {
params: Pick<CallbackDataParams, 'name' | 'value' | 'percent'>;
labelType: EchartsPieLabelType;
- numberFormatter: NumberFormatter;
+ numberFormatter: ValueFormatter;
sanitizeName?: boolean;
}): string {
const { name: rawName = '', value, percent } = params;
@@ -145,7 +146,9 @@ export default function transformProps(
theme,
inContextMenu,
emitCrossFilters,
+ datasource,
} = chartProps;
+ const { columnFormats = {}, currencyFormats = {} } = datasource;
const { data = [] } = queriesData[0];
const coltypeMapping = getColtypesMapping(queriesData[0]);
@@ -203,7 +206,13 @@ export default function transformProps(
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
- const numberFormatter = getNumberFormatter(numberFormat);
+ const numberFormatter = getValueFormatter(
+ metric,
+ currencyFormats,
+ columnFormats,
+ numberFormat,
+ );
+
let totalValue = 0;
const transformedData: PieSeriesOption[] = data.map(datum => {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index c89bff2e8c..2066148c84 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -22,6 +22,7 @@ import {
AnnotationLayer,
AxisType,
CategoricalColorNamespace,
+ CurrencyFormatter,
ensureIsArray,
GenericDataType,
getMetricLabel,
@@ -32,9 +33,13 @@ import {
isFormulaAnnotationLayer,
isIntervalAnnotationLayer,
isPhysicalColumn,
+ isSavedMetric,
isTimeseriesAnnotationLayer,
+ NumberFormats,
+ QueryFormMetric,
t,
TimeseriesChartDataResponseResult,
+ ValueFormatter,
} from '@superset-ui/core';
import {
extractExtraMetrics,
@@ -92,6 +97,36 @@ import {
TIMEGRAIN_TO_TIMESTAMP,
} from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ buildCustomFormatters,
+ getCustomFormatter,
+} from '../utils/valueFormatter';
+
+const getYAxisFormatter = (
+ metrics: QueryFormMetric[],
+ forcePercentFormatter: boolean,
+ customFormatters: Record<string, ValueFormatter>,
+ yAxisFormat: string = NumberFormats.SMART_NUMBER,
+) => {
+ if (forcePercentFormatter) {
+ return getNumberFormatter(',.0%');
+ }
+ const metricsArray = ensureIsArray(metrics);
+ if (
+ metricsArray.every(isSavedMetric) &&
+ metricsArray
+ .map(metric => customFormatters[metric])
+ .every(
+ (formatter, _, formatters) =>
+ formatter instanceof CurrencyFormatter &&
+ (formatter as CurrencyFormatter)?.currency?.symbol ===
+ (formatters[0] as CurrencyFormatter)?.currency?.symbol,
+ )
+ ) {
+ return customFormatters[metricsArray[0]];
+ }
+ return getNumberFormatter(yAxisFormat);
+};
export default function transformProps(
chartProps: EchartsTimeseriesChartProps,
@@ -109,7 +144,11 @@ export default function transformProps(
inContextMenu,
emitCrossFilters,
} = chartProps;
- const { verboseMap = {} } = datasource;
+ const {
+ verboseMap = {},
+ columnFormats = {},
+ currencyFormats = {},
+ } = datasource;
const [queryData] = queriesData;
const { data = [], label_map = {} } =
queryData as TimeseriesChartDataResponseResult;
@@ -232,8 +271,15 @@ export default function transformProps(
const xAxisType = getAxisType(xAxisDataType);
const series: SeriesOption[] = [];
- const formatter = getNumberFormatter(
- contributionMode || isAreaExpand ? ',.0%' : yAxisFormat,
+
+ const forcePercentFormatter = Boolean(contributionMode || isAreaExpand);
+ const percentFormatter = getNumberFormatter(',.0%');
+ const defaultFormatter = getNumberFormatter(yAxisFormat);
+ const customFormatters = buildCustomFormatters(
+ metrics,
+ currencyFormats,
+ columnFormats,
+ yAxisFormat,
);
const array = ensureIsArray(chartProps.rawFormData?.time_compare);
@@ -262,7 +308,13 @@ export default function transformProps(
seriesType,
legendState,
stack,
- formatter,
+ formatter: forcePercentFormatter
+ ? percentFormatter
+ : getCustomFormatter(
+ customFormatters,
+ metrics,
+ labelMap[seriesName]?.[0],
+ ) ?? defaultFormatter,
showValue,
onlyTotal,
totalStackedValues: sortedTotalValues,
@@ -440,7 +492,14 @@ export default function transformProps(
max,
minorTick: { show: true },
minorSplitLine: { show: minorSplitLine },
- axisLabel: { formatter },
+ axisLabel: {
+ formatter: getYAxisFormatter(
+ metrics,
+ forcePercentFormatter,
+ customFormatters,
+ yAxisFormat,
+ ),
+ },
scale: truncateYAxis,
name: yAxisTitle,
nameGap: convertInteger(yAxisTitleMargin),
@@ -485,10 +544,17 @@ export default function transformProps(
if (value.observation === 0 && stack) {
return;
}
+ // if there are no dimensions, key is a verbose name of a metric,
+ // otherwise it is a comma separated string where the first part is metric name
+ const formatterKey =
+ groupby.length === 0 ? inverted[key] : labelMap[key]?.[0];
const content = formatForecastTooltipSeries({
...value,
seriesName: key,
- formatter,
+ formatter: forcePercentFormatter
+ ? percentFormatter
+ : getCustomFormatter(customFormatters, metrics, formatterKey) ??
+ defaultFormatter,
});
if (!legendState || legendState[key]) {
rows.push(`<span style="font-weight: 700">${content}</span>`);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
index fb4739dc74..0bcc5baf8d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
@@ -28,13 +28,13 @@ import {
IntervalAnnotationLayer,
isTimeseriesAnnotationResult,
LegendState,
- NumberFormatter,
smartDateDetailedFormatter,
smartDateFormatter,
SupersetTheme,
TimeFormatter,
TimeseriesAnnotationLayer,
TimeseriesDataRecord,
+ ValueFormatter,
} from '@superset-ui/core';
import { SeriesOption } from 'echarts';
import {
@@ -158,7 +158,7 @@ export function transformSeries(
showValue?: boolean;
onlyTotal?: boolean;
legendState?: LegendState;
- formatter?: NumberFormatter;
+ formatter?: ValueFormatter;
totalStackedValues?: number[];
showValueIndexes?: number[];
thresholdValues?: number[];
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
index 89088be5fa..9e0454b386 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
@@ -23,7 +23,7 @@ import {
getNumberFormatter,
getTimeFormatter,
NumberFormats,
- NumberFormatter,
+ ValueFormatter,
} from '@superset-ui/core';
import { TreemapSeriesNodeItemOption } from 'echarts/types/src/chart/treemap/TreemapSeries';
import { EChartsCoreOption, TreemapSeriesOption } from 'echarts';
@@ -48,6 +48,7 @@ import { OpacityEnum } from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
import { treeBuilder, TreeNode } from '../utils/treeBuilder';
+import { getValueFormatter } from '../utils/valueFormatter';
export function formatLabel({
params,
@@ -56,7 +57,7 @@ export function formatLabel({
}: {
params: TreemapSeriesCallbackDataParams;
labelType: EchartsTreemapLabelType;
- numberFormatter: NumberFormatter;
+ numberFormatter: ValueFormatter;
}): string {
const { name = '', value } = params;
const formattedValue = numberFormatter(value as number);
@@ -78,7 +79,7 @@ export function formatTooltip({
numberFormatter,
}: {
params: TreemapSeriesCallbackDataParams;
- numberFormatter: NumberFormatter;
+ numberFormatter: ValueFormatter;
}): string {
const { value, treePathInfo = [] } = params;
const formattedValue = numberFormatter(value as number);
@@ -118,8 +119,10 @@ export default function transformProps(
theme,
inContextMenu,
emitCrossFilters,
+ datasource,
} = chartProps;
const { data = [] } = queriesData[0];
+ const { columnFormats = {}, currencyFormats = {} } = datasource;
const { setDataMask = () => {}, onContextMenu } = hooks;
const coltypeMapping = getColtypesMapping(queriesData[0]);
@@ -141,7 +144,13 @@ export default function transformProps(
};
const refs: Refs = {};
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
- const numberFormatter = getNumberFormatter(numberFormat);
+ const numberFormatter = getValueFormatter(
+ metric,
+ currencyFormats,
+ columnFormats,
+ numberFormat,
+ );
+
const formatter = (params: TreemapSeriesCallbackDataParams) =>
formatLabel({
params,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
index 485e9fb896..18b160b9c6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import { isNumber } from 'lodash';
-import { DataRecord, DTTM_ALIAS, NumberFormatter } from '@superset-ui/core';
+import { DataRecord, DTTM_ALIAS, ValueFormatter } from '@superset-ui/core';
import { OptionName } from 'echarts/types/src/util/types';
import { TooltipMarker } from 'echarts/types/src/util/format';
import {
@@ -91,7 +91,7 @@ export const formatForecastTooltipSeries = ({
}: ForecastValue & {
seriesName: string;
marker: TooltipMarker;
- formatter: NumberFormatter;
+ formatter: ValueFormatter;
}): string => {
let row = `${marker}${sanitizeHtml(seriesName)}: `;
let isObservation = false;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index 0f6efd72f2..f9477ea25a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -31,6 +31,7 @@ import {
SupersetTheme,
normalizeTimestamp,
LegendState,
+ ValueFormatter,
} from '@superset-ui/core';
import { SortSeriesType } from '@superset-ui/chart-controls';
import { format, LegendComponentOption, SeriesOption } from 'echarts';
@@ -345,7 +346,7 @@ export function formatSeriesName(
timeFormatter,
coltype,
}: {
- numberFormatter?: NumberFormatter;
+ numberFormatter?: ValueFormatter;
timeFormatter?: TimeFormatter;
coltype?: GenericDataType;
} = {},
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/valueFormatter.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/valueFormatter.ts
new file mode 100644
index 0000000000..5d995c9f00
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/valueFormatter.ts
@@ -0,0 +1,63 @@
+import {
+ Currency,
+ CurrencyFormatter,
+ ensureIsArray,
+ getNumberFormatter,
+ isSavedMetric,
+ QueryFormMetric,
+ ValueFormatter,
+} from '@superset-ui/core';
+
+export const buildCustomFormatters = (
+ metrics: QueryFormMetric | QueryFormMetric[] | undefined,
+ currencyFormats: Record<string, Currency>,
+ columnFormats: Record<string, string>,
+ d3Format: string | undefined,
+) => {
+ const metricsArray = ensureIsArray(metrics);
+ return metricsArray.reduce((acc, metric) => {
+ const actualD3Format = isSavedMetric(metric)
+ ? columnFormats[metric] ?? d3Format
+ : d3Format;
+ if (isSavedMetric(metric)) {
+ return currencyFormats[metric]
+ ? {
+ ...acc,
+ [metric]: new CurrencyFormatter({
+ d3Format: actualD3Format,
+ currency: currencyFormats[metric],
+ }),
+ }
+ : {
+ ...acc,
+ [metric]: getNumberFormatter(actualD3Format),
+ };
+ }
+ return acc;
+ }, {});
+};
+
+export const getCustomFormatter = (
+ customFormatters: Record<string, ValueFormatter>,
+ metrics: QueryFormMetric | QueryFormMetric[] | undefined,
+ key?: string,
+) => {
+ const metricsArray = ensureIsArray(metrics);
+ if (metricsArray.length === 1 && isSavedMetric(metricsArray[0])) {
+ return customFormatters[metricsArray[0]];
+ }
+ return key ? customFormatters[key] : undefined;
+};
+
+export const getValueFormatter = (
+ metrics: QueryFormMetric | QueryFormMetric[] | undefined,
+ currencyFormats: Record<string, Currency>,
+ columnFormats: Record<string, string>,
+ d3Format: string | undefined,
+ key?: string,
+) =>
+ getCustomFormatter(
+ buildCustomFormatters(metrics, currencyFormats, columnFormats, d3Format),
+ metrics,
+ key,
+ ) ?? getNumberFormatter(d3Format);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts
index ce00bb7190..bdbbbcd9d1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts
@@ -158,5 +158,30 @@ describe('BigNumberWithTrendline', () => {
'1.23',
);
});
+
+ it('should format with datasource currency', () => {
+ const propsWithDatasource = {
+ ...props,
+ datasource: {
+ ...props.datasource,
+ currencyFormats: {
+ value: { symbol: 'USD', symbolPosition: 'prefix' },
+ },
+ metrics: [
+ {
+ label: 'value',
+ metric_name: 'value',
+ d3format: '.2f',
+ currency: `{symbol: 'USD', symbolPosition: 'prefix' }`,
+ },
+ ],
+ },
+ };
+ const transformed = transformProps(propsWithDatasource);
+ // @ts-ignore
+ expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual(
+ '$ 1.23',
+ );
+ });
});
});
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
index a5610756b2..f463990b1d 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
@@ -21,6 +21,7 @@ import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
import {
AdhocMetric,
BinaryQueryObjectFilterClause,
+ CurrencyFormatter,
DataRecordValue,
FeatureFlag,
getColumnLabel,
@@ -144,6 +145,7 @@ export default function PivotTableChart(props: PivotTableProps) {
selectedFilters,
verboseMap,
columnFormats,
+ currencyFormats,
metricsLayout,
metricColorFormatters,
dateFormatters,
@@ -156,24 +158,39 @@ export default function PivotTableChart(props: PivotTableProps) {
() => getNumberFormatter(valueFormat),
[valueFormat],
);
- const columnFormatsArray = useMemo(
- () => Object.entries(columnFormats),
- [columnFormats],
+ const customFormatsArray = useMemo(
+ () =>
+ Array.from(
+ new Set([
+ ...Object.keys(columnFormats || {}),
+ ...Object.keys(currencyFormats || {}),
+ ]),
+ ).map(metricName => [
+ metricName,
+ columnFormats[metricName] || valueFormat,
+ currencyFormats[metricName],
+ ]),
+ [columnFormats, currencyFormats, valueFormat],
);
- const hasCustomMetricFormatters = columnFormatsArray.length > 0;
+ const hasCustomMetricFormatters = customFormatsArray.length > 0;
const metricFormatters = useMemo(
() =>
hasCustomMetricFormatters
? {
[METRIC_KEY]: Object.fromEntries(
- columnFormatsArray.map(([metric, format]) => [
+ customFormatsArray.map(([metric, d3Format, currency]) => [
metric,
- getNumberFormatter(format),
+ currency
+ ? new CurrencyFormatter({
+ currency,
+ d3Format,
+ })
+ : getNumberFormatter(d3Format),
]),
),
}
: undefined,
- [columnFormatsArray, hasCustomMetricFormatters],
+ [customFormatsArray, hasCustomMetricFormatters],
);
const metricNames = useMemo(
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
index 43d73e6193..f335c6978e 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
@@ -79,7 +79,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
rawFormData,
hooks: { setDataMask = () => {}, onContextMenu },
filterState,
- datasource: { verboseMap = {}, columnFormats = {} },
+ datasource: { verboseMap = {}, columnFormats = {}, currencyFormats = {} },
emitCrossFilters,
} = chartProps;
const { data, colnames, coltypes } = queriesData[0];
@@ -162,6 +162,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
selectedFilters,
verboseMap,
columnFormats,
+ currencyFormats,
metricsLayout,
metricColorFormatters,
dateFormatters,
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
index e011f45931..dea5236666 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
@@ -28,6 +28,7 @@ import {
QueryFormColumn,
TimeGranularity,
ContextMenuFilters,
+ Currency,
} from '@superset-ui/core';
import { ColorFormatters } from '@superset-ui/chart-controls';
@@ -69,6 +70,7 @@ interface PivotTableCustomizeProps {
selectedFilters?: SelectedFiltersType;
verboseMap: JsonObject;
columnFormats: JsonObject;
+ currencyFormats: Record<string, Currency>;
metricsLayout?: MetricsLayoutEnum;
metricColorFormatters: ColorFormatters;
dateFormatters: Record<string, DateFormatter | undefined>;
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts
index 770cb9849b..f3ac8b82d4 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts
@@ -44,6 +44,7 @@ describe('PivotTableChart buildQuery', () => {
combineMetric: false,
verboseMap: {},
columnFormats: {},
+ currencyFormats: {},
metricColorFormatters: [],
dateFormatters: {},
setDataMask: () => {},
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
index 3edb4619af..91fc5d260e 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
@@ -90,6 +90,7 @@ describe('PivotTableChart transformProps', () => {
dateFormatters: {},
emitCrossFilters: false,
columnFormats: {},
+ currencyFormats: {},
});
});
});
diff --git a/superset-frontend/plugins/plugin-chart-table/package.json b/superset-frontend/plugins/plugin-chart-table/package.json
index ac3b175660..54b209c2c5 100644
--- a/superset-frontend/plugins/plugin-chart-table/package.json
+++ b/superset-frontend/plugins/plugin-chart-table/package.json
@@ -2,30 +2,27 @@
"name": "@superset-ui/plugin-chart-table",
"version": "0.18.25",
"description": "Superset Chart - Table",
- "main": "lib/index.js",
- "module": "esm/index.js",
- "sideEffects": false,
- "files": [
- "esm",
- "lib"
- ],
- "repository": {
- "type": "git",
- "url": "https://github.com/apache/superset.git",
- "directory": "superset-frontend/plugins/plugin-chart-table"
- },
"keywords": [
"superset"
],
- "author": "Superset",
- "license": "Apache-2.0",
+ "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-table#readme",
"bugs": {
"url": "https://github.com/apache/superset/issues"
},
- "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-table#readme",
- "publishConfig": {
- "access": "public"
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/apache/superset.git",
+ "directory": "superset-frontend/plugins/plugin-chart-table"
},
+ "license": "Apache-2.0",
+ "author": "Superset",
+ "sideEffects": false,
+ "main": "lib/index.js",
+ "module": "esm/index.js",
+ "files": [
+ "esm",
+ "lib"
+ ],
"dependencies": {
"@react-icons/all-files": "^4.1.0",
"@types/d3-array": "^2.9.0",
@@ -40,15 +37,20 @@
"regenerator-runtime": "^0.13.7",
"xss": "^1.0.10"
},
- "devDependencies": {
- "@testing-library/react": "^11.2.0"
- },
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
+ "@testing-library/dom": "^7.29.4",
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.0",
+ "@testing-library/react-hooks": "^5.0.3",
+ "@testing-library/user-event": "^12.7.0",
"@types/classnames": "*",
"@types/react": "*",
"react": "^16.13.1",
"react-dom": "^16.13.1"
+ },
+ "publishConfig": {
+ "access": "public"
}
}
diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
index e03667a695..dfb398cab0 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
@@ -18,6 +18,7 @@
*/
import memoizeOne from 'memoize-one';
import {
+ CurrencyFormatter,
DataRecord,
extractTimegrain,
GenericDataType,
@@ -84,7 +85,7 @@ const processColumns = memoizeOne(function processColumns(
props: TableChartProps,
) {
const {
- datasource: { columnFormats, verboseMap },
+ datasource: { columnFormats, currencyFormats, verboseMap },
rawFormData: {
table_timestamp_format: tableTimestampFormat,
metrics: metrics_,
@@ -123,6 +124,7 @@ const processColumns = memoizeOne(function processColumns(
const isTime = dataType === GenericDataType.TEMPORAL;
const isNumber = dataType === GenericDataType.NUMERIC;
const savedFormat = columnFormats?.[key];
+ const currency = currencyFormats?.[key];
const numberFormat = config.d3NumberFormat || savedFormat;
let formatter;
@@ -155,7 +157,9 @@ const processColumns = memoizeOne(function processColumns(
// percent metrics have a default format
formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
} else if (isMetric || (isNumber && numberFormat)) {
- formatter = getNumberFormatter(numberFormat);
+ formatter = currency
+ ? new CurrencyFormatter({ d3Format: numberFormat, currency })
+ : getNumberFormatter(numberFormat);
}
return {
key,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts
index f76d2718b4..35a463fe2c 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts
@@ -31,6 +31,7 @@ import {
QueryFormData,
SetDataMaskHook,
ContextMenuFilters,
+ CurrencyFormatter,
} from '@superset-ui/core';
import { ColorFormatters, ColumnConfig } from '@superset-ui/chart-controls';
@@ -42,7 +43,11 @@ export interface DataColumnMeta {
// `label` is verbose column name used for rendering
label: string;
dataType: GenericDataType;
- formatter?: TimeFormatter | NumberFormatter | CustomFormatter;
+ formatter?:
+ | TimeFormatter
+ | NumberFormatter
+ | CustomFormatter
+ | CurrencyFormatter;
isMetric?: boolean;
isPercentMetric?: boolean;
isNumeric?: boolean;
diff --git a/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts b/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
index 607afa8ac3..139f92336c 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
@@ -47,7 +47,6 @@ function formatValue(
return [false, 'N/A'];
}
if (formatter) {
- // in case percent metric can specify percent format in the future
return [false, formatter(value as number)];
}
if (typeof value === 'string') {
diff --git a/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts b/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
index bd4b704391..fc060e6138 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
@@ -27,6 +27,7 @@ export default function isEqualColumns(
const b = propsB[0];
return (
a.datasource.columnFormats === b.datasource.columnFormats &&
+ a.datasource.currencyFormats === b.datasource.currencyFormats &&
a.datasource.verboseMap === b.datasource.verboseMap &&
a.formData.tableTimestampFormat === b.formData.tableTimestampFormat &&
a.formData.timeGrainSqla === b.formData.timeGrainSqla &&
diff --git a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
index 1e699b6888..bd859b467c 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
@@ -19,6 +19,7 @@
import React from 'react';
import { CommonWrapper } from 'enzyme';
import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
import TableChart from '../src/TableChart';
import transformProps from '../src/transformProps';
import DateWithFormatter from '../src/utils/DateWithFormatter';
@@ -102,6 +103,26 @@ describe('plugin-chart-table', () => {
expect(cells.eq(4).text()).toEqual('2.47k');
});
+ it('render advanced data with currencies', () => {
+ render(
+ ProviderWrapper({
+ children: (
+ <TableChart
+ {...transformProps(testData.advancedWithCurrency)}
+ sticky={false}
+ />
+ ),
+ }),
+ );
+ const cells = document.querySelectorAll('td');
+ expect(document.querySelectorAll('th')[1]).toHaveTextContent(
+ 'Sum of Num',
+ );
+ expect(cells[0]).toHaveTextContent('Michael');
+ expect(cells[2]).toHaveTextContent('12.346%');
+ expect(cells[4]).toHaveTextContent('$ 2.47k');
+ });
+
it('render empty data', () => {
wrap.setProps({ ...transformProps(testData.empty), sticky: false });
tree = wrap.render();
diff --git a/superset-frontend/plugins/plugin-chart-table/test/testData.ts b/superset-frontend/plugins/plugin-chart-table/test/testData.ts
index 9896e7bf49..3f464181b7 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/testData.ts
+++ b/superset-frontend/plugins/plugin-chart-table/test/testData.ts
@@ -173,6 +173,16 @@ const advanced: TableChartProps = {
],
};
+const advancedWithCurrency = {
+ ...advanced,
+ datasource: {
+ ...advanced.datasource,
+ currencyFormats: {
+ sum__num: { symbol: 'USD', symbolPosition: 'prefix' },
+ },
+ },
+};
+
const empty = {
...advanced,
queriesData: [
@@ -186,5 +196,6 @@ const empty = {
export default {
basic,
advanced,
+ advancedWithCurrency,
empty,
};
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index 79b10b8fc7..d95839d972 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -25,6 +25,9 @@ import Alert from 'src/components/Alert';
import Badge from 'src/components/Badge';
import shortid from 'shortid';
import {
+ css,
+ getCurrencySymbol,
+ ensureIsArray,
FeatureFlag,
styled,
SupersetClient,
@@ -146,6 +149,11 @@ const DATA_TYPES = [
{ value: 'BOOLEAN', label: t('BOOLEAN') },
];
+const CURRENCY_SYMBOL_POSITION = [
+ { value: 'prefix', label: t('Prefix') },
+ { value: 'suffix', label: t('Suffix') },
+];
+
const DATASOURCE_TYPES_ARR = [
{ key: 'physical', label: t('Physical (table or view)') },
{ key: 'virtual', label: t('Virtual (SQL)') },
@@ -572,6 +580,43 @@ function OwnersSelector({ datasource, onChange }) {
);
}
+const CurrencyControlContainer = styled.div`
+ ${({ theme }) => css`
+ display: flex;
+ align-items: center;
+
+ & > :first-child {
+ width: 25%;
+ margin-right: ${theme.gridUnit * 4}px;
+ }
+ `}
+`;
+const CurrencyControl = ({ onChange, value: currency = {}, currencies }) => (
+ <CurrencyControlContainer>
+ <Select
+ ariaLabel={t('Currency prefix or suffix')}
+ options={CURRENCY_SYMBOL_POSITION}
+ placeholder={t('Prefix or suffix')}
+ onChange={symbolPosition => {
+ onChange({ ...currency, symbolPosition });
+ }}
+ value={currency?.symbolPosition}
+ allowClear
+ />
+ <Select
+ ariaLabel={t('Currency symbol')}
+ options={currencies}
+ placeholder={t('Select or type currency symbol')}
+ onChange={symbol => {
+ onChange({ ...currency, symbol });
+ }}
+ value={currency?.symbol}
+ allowClear
+ allowNewOptions
+ />
+ </CurrencyControlContainer>
+);
+
class DatasourceEditor extends React.PureComponent {
constructor(props) {
super(props);
@@ -628,6 +673,12 @@ class DatasourceEditor extends React.PureComponent {
this.allowEditSource = !isFeatureEnabled(
FeatureFlag.DISABLE_DATASET_SOURCE_EDIT,
);
+ this.currencies = ensureIsArray(props.currencies).map(currencyCode => ({
+ value: currencyCode,
+ label: `${getCurrencySymbol({
+ symbol: currencyCode,
+ })} (${currencyCode})`,
+ }));
}
onChange() {
@@ -839,6 +890,20 @@ class DatasourceEditor extends React.PureComponent {
),
);
+ // validate currency code
+ try {
+ this.state.datasource.metrics?.forEach(
+ metric =>
+ metric.currency?.symbol &&
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: metric.currency.symbol,
+ }),
+ );
+ } catch {
+ errors = errors.concat([t('Invalid currency code in saved metrics')]);
+ }
+
this.setState({ errors }, callback);
}
@@ -1228,6 +1293,11 @@ class DatasourceEditor extends React.PureComponent {
<TextControl controlId="d3format" placeholder="%y/%m/%d" />
}
/>
+ <Field
+ fieldKey="currency"
+ label={t('Metric currency')}
+ control={<CurrencyControl currencies={this.currencies} />}
+ />
<Field
label={t('Certified by')}
fieldKey="certified_by"
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx
index f598290ba8..abb77c7f19 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx
@@ -28,7 +28,7 @@ const props = {
datasource: mockDatasource['7__table'],
addSuccessToast: () => {},
addDangerToast: () => {},
- onChange: () => {},
+ onChange: jest.fn(),
columnLabels: {
state: 'State',
},
@@ -217,6 +217,90 @@ describe('DatasourceEditor RTL', () => {
const warningMarkdown = await screen.findByPlaceholderText(/certified by/i);
expect(warningMarkdown.value).toEqual('someone');
});
+ it('renders currency controls', async () => {
+ const propsWithCurrency = {
+ ...props,
+ currencies: ['USD', 'GBP', 'EUR'],
+ datasource: {
+ ...props.datasource,
+ metrics: [
+ {
+ ...props.datasource.metrics[0],
+ currency: { symbol: 'USD', symbolPosition: 'prefix' },
+ },
+ ...props.datasource.metrics.slice(1),
+ ],
+ },
+ };
+ await asyncRender(propsWithCurrency);
+ const metricButton = screen.getByTestId('collection-tab-Metrics');
+ userEvent.click(metricButton);
+ const expandToggle = await screen.findAllByLabelText(/toggle expand/i);
+ userEvent.click(expandToggle[0]);
+
+ expect(await screen.findByText('Metric currency')).toBeVisible();
+ expect(
+ await waitFor(() =>
+ document.querySelector(
+ `[aria-label='Currency prefix or suffix'] .ant-select-selection-item`,
+ ),
+ ),
+ ).toHaveTextContent('Prefix');
+ await userEvent.click(
+ screen.getByRole('combobox', { name: 'Currency prefix or suffix' }),
+ );
+ const positionOptions = await waitFor(() =>
+ document.querySelectorAll(
+ `[aria-label='Currency prefix or suffix'] .ant-select-item-option-content`,
+ ),
+ );
+ expect(positionOptions[0]).toHaveTextContent('Prefix');
+ expect(positionOptions[1]).toHaveTextContent('Suffix');
+
+ propsWithCurrency.onChange.mockClear();
+ await userEvent.click(positionOptions[1]);
+ expect(propsWithCurrency.onChange.mock.calls[0][0]).toMatchObject(
+ expect.objectContaining({
+ metrics: expect.arrayContaining([
+ expect.objectContaining({
+ currency: { symbolPosition: 'suffix', symbol: 'USD' },
+ }),
+ ]),
+ }),
+ );
+
+ expect(
+ await waitFor(() =>
+ document.querySelector(
+ `[aria-label='Currency symbol'] .ant-select-selection-item`,
+ ),
+ ),
+ ).toHaveTextContent('$ (USD)');
+
+ propsWithCurrency.onChange.mockClear();
+ await userEvent.click(
+ screen.getByRole('combobox', { name: 'Currency symbol' }),
+ );
+ const symbolOptions = await waitFor(() =>
+ document.querySelectorAll(
+ `[aria-label='Currency symbol'] .ant-select-item-option-content`,
+ ),
+ );
+ expect(symbolOptions[0]).toHaveTextContent('$ (USD)');
+ expect(symbolOptions[1]).toHaveTextContent('£ (GBP)');
+ expect(symbolOptions[2]).toHaveTextContent('€ (EUR)');
+
+ await userEvent.click(symbolOptions[1]);
+ expect(propsWithCurrency.onChange.mock.calls[0][0]).toMatchObject(
+ expect.objectContaining({
+ metrics: expect.arrayContaining([
+ expect.objectContaining({
+ currency: { symbolPosition: 'suffix', symbol: 'GBP' },
+ }),
+ ]),
+ }),
+ );
+ });
it('properly updates the metric information', async () => {
await asyncRender(props);
const metricButton = screen.getByTestId('collection-tab-Metrics');
diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx
index 1a7bf227ec..c5063890cb 100644
--- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx
+++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx
@@ -19,7 +19,14 @@
import React, { FunctionComponent, useState, useRef } from 'react';
import Alert from 'src/components/Alert';
import Button from 'src/components/Button';
-import { FeatureFlag, styled, SupersetClient, t } from '@superset-ui/core';
+import {
+ FeatureFlag,
+ isDefined,
+ Metric,
+ styled,
+ SupersetClient,
+ t,
+} from '@superset-ui/core';
import Modal from 'src/components/Modal';
import AsyncEsmComponent from 'src/components/AsyncEsmComponent';
@@ -27,6 +34,7 @@ import { isFeatureEnabled } from 'src/featureFlags';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import withToasts from 'src/components/MessageToasts/withToasts';
+import { useSelector } from 'react-redux';
const DatasourceEditor = AsyncEsmComponent(() => import('./DatasourceEditor'));
@@ -81,7 +89,21 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
onHide,
show,
}) => {
- const [currentDatasource, setCurrentDatasource] = useState(datasource);
+ const [currentDatasource, setCurrentDatasource] = useState({
+ ...datasource,
+ metrics: datasource?.metrics?.map((metric: Metric) => ({
+ ...metric,
+ currency: JSON.parse(metric.currency || 'null'),
+ })),
+ });
+ const currencies = useSelector<
+ {
+ common: {
+ currencies: string[];
+ };
+ },
+ string[]
+ >(state => state.common?.currencies);
const [errors, setErrors] = useState<any[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
@@ -125,7 +147,10 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
description: metric.description,
metric_name: metric.metric_name,
metric_type: metric.metric_type,
- d3format: metric.d3format,
+ d3format: metric.d3format || null,
+ currency: !isDefined(metric.currency)
+ ? null
+ : JSON.stringify(metric.currency),
verbose_name: metric.verbose_name,
warning_text: metric.warning_text,
uuid: metric.uuid,
@@ -297,6 +322,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
datasource={currentDatasource}
onChange={onDatasourceChange}
setIsEditing={setIsEditing}
+ currencies={currencies}
/>
{contextHolder}
</StyledDatasourceModal>
diff --git a/superset-frontend/src/dashboard/constants.ts b/superset-frontend/src/dashboard/constants.ts
index 588e5b6cd5..ecc893bcb7 100644
--- a/superset-frontend/src/dashboard/constants.ts
+++ b/superset-frontend/src/dashboard/constants.ts
@@ -29,6 +29,7 @@ export const PLACEHOLDER_DATASOURCE: Datasource = {
column_types: [],
metrics: [],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '',
description: '',
diff --git a/superset-frontend/src/explore/actions/datasourcesActions.test.ts b/superset-frontend/src/explore/actions/datasourcesActions.test.ts
index bca3aecfd6..a844ff4789 100644
--- a/superset-frontend/src/explore/actions/datasourcesActions.test.ts
+++ b/superset-frontend/src/explore/actions/datasourcesActions.test.ts
@@ -35,6 +35,7 @@ const CURRENT_DATASOURCE = {
columns: [],
metrics: [],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '__timestamp',
// eg. ['["ds", true]', 'ds [asc]']
@@ -48,6 +49,7 @@ const NEW_DATASOURCE = {
columns: [],
metrics: [],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '__timestamp',
// eg. ['["ds", true]', 'ds [asc]']
diff --git a/superset-frontend/src/explore/actions/hydrateExplore.ts b/superset-frontend/src/explore/actions/hydrateExplore.ts
index e259f62671..bced548b8d 100644
--- a/superset-frontend/src/explore/actions/hydrateExplore.ts
+++ b/superset-frontend/src/explore/actions/hydrateExplore.ts
@@ -96,6 +96,7 @@ export const hydrateExplore =
if (dashboardId) {
initialFormData.dashboardId = dashboardId;
}
+
const initialDatasource = dataset;
const initialExploreState = {
diff --git a/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx b/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
index e03aba06d1..c18873460a 100644
--- a/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
+++ b/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
@@ -52,6 +52,7 @@ describe('controlUtils', () => {
columns: [{ column_name: 'a' }],
metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '',
datasource_name: '1__table',
diff --git a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
index c8d34f749d..f0eb399267 100644
--- a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
+++ b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
@@ -35,6 +35,7 @@ const sampleDatasource: Dataset = {
],
metrics: [{ metric_name: 'saved_metric_2' }],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '',
datasource_name: 'Sample Dataset',
diff --git a/superset-frontend/src/explore/fixtures.tsx b/superset-frontend/src/explore/fixtures.tsx
index a0f3c112ec..7502094ca2 100644
--- a/superset-frontend/src/explore/fixtures.tsx
+++ b/superset-frontend/src/explore/fixtures.tsx
@@ -136,6 +136,7 @@ export const exploreInitialData: ExplorePageInitialData = {
columns: [{ column_name: 'a' }],
metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '',
datasource_name: '8__table',
@@ -154,6 +155,7 @@ export const fallbackExploreInitialData: ExplorePageInitialData = {
columns: [],
metrics: [],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '',
owners: [],
diff --git a/superset-frontend/src/features/datasets/types.ts b/superset-frontend/src/features/datasets/types.ts
index 97d6f5a280..4c2c5b8a95 100644
--- a/superset-frontend/src/features/datasets/types.ts
+++ b/superset-frontend/src/features/datasets/types.ts
@@ -1,3 +1,5 @@
+import { Currency } from '@superset-ui/core';
+
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -39,6 +41,7 @@ type MetricObject = {
metric_name: string;
metric_type: string;
d3format?: string;
+ currency?: Currency;
warning_text?: string;
};
diff --git a/superset-frontend/src/utils/getDatasourceUid.test.ts b/superset-frontend/src/utils/getDatasourceUid.test.ts
index d3a629efcf..ed7ec6256b 100644
--- a/superset-frontend/src/utils/getDatasourceUid.test.ts
+++ b/superset-frontend/src/utils/getDatasourceUid.test.ts
@@ -26,6 +26,7 @@ const TEST_DATASOURCE = {
columns: [],
metrics: [],
column_formats: {},
+ currency_formats: {},
verbose_map: {},
main_dttm_col: '__timestamp',
// eg. ['["ds", true]', 'ds [asc]']
diff --git a/superset/config.py b/superset/config.py
index b880acf5a7..abb73e9f56 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -374,6 +374,8 @@ class D3Format(TypedDict, total=False):
D3_FORMAT: D3Format = {}
+CURRENCIES = ["USD", "EUR", "GBP", "INR", "MXN", "JPY", "CNY"]
+
# ---------------------------------------------------
# Feature flags
# ---------------------------------------------------
diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index 647df374c9..56e5ef18ad 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -18,9 +18,11 @@ from __future__ import annotations
import builtins
import json
+import logging
from collections.abc import Hashable
from datetime import datetime
from enum import Enum
+from json.decoder import JSONDecodeError
from typing import Any, TYPE_CHECKING
from flask_appbuilder.security.sqla.models import User
@@ -47,6 +49,8 @@ from superset.utils.core import GenericDataType, MediumText
if TYPE_CHECKING:
from superset.db_engine_specs.base import BaseEngineSpec
+logger = logging.getLogger(__name__)
+
METRIC_FORM_DATA_PARAMS = [
"metric",
"metric_2",
@@ -224,6 +228,10 @@ class BaseDatasource(
def column_formats(self) -> dict[str, str | None]:
return {m.metric_name: m.d3format for m in self.metrics if m.d3format}
+ @property
+ def currency_formats(self) -> dict[str, dict[str, str | None] | None]:
+ return {m.metric_name: m.currency_json for m in self.metrics if m.currency_json}
+
def add_missing_metrics(self, metrics: list[BaseMetric]) -> None:
existing_metrics = {m.metric_name for m in self.metrics}
for metric in metrics:
@@ -282,6 +290,7 @@ class BaseDatasource(
"id": self.id,
"uid": self.uid,
"column_formats": self.column_formats,
+ "currency_formats": self.currency_formats,
"description": self.description,
"database": self.database.data, # pylint: disable=no-member
"default_endpoint": self.default_endpoint,
@@ -717,6 +726,7 @@ class BaseMetric(AuditMixinNullable, ImportExportMixin):
metric_type = Column(String(32))
description = Column(MediumText())
d3format = Column(String(128))
+ currency = Column(String(128))
warning_text = Column(Text)
"""
@@ -733,6 +743,16 @@ class BaseMetric(AuditMixinNullable, ImportExportMixin):
enable_typechecks=False)
"""
+ @property
+ def currency_json(self) -> dict[str, str | None] | None:
+ try:
+ return json.loads(self.currency or "{}") or None
+ except (TypeError, JSONDecodeError) as exc:
+ logger.error(
+ "Unable to load currency json: %r. Leaving empty.", exc, exc_info=True
+ )
+ return None
+
@property
def perm(self) -> str | None:
raise NotImplementedError()
@@ -751,5 +771,6 @@ class BaseMetric(AuditMixinNullable, ImportExportMixin):
"expression",
"warning_text",
"d3format",
+ "currency",
)
return {s: getattr(self, s) for s in attrs}
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 4eebec6be7..9efbd1db92 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -416,6 +416,7 @@ class SqlMetric(Model, BaseMetric, CertificationMixin):
"expression",
"description",
"d3format",
+ "currency",
"extra",
"warning_text",
]
diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py
index 9116b9636e..65c6f110e4 100644
--- a/superset/connectors/sqla/views.py
+++ b/superset/connectors/sqla/views.py
@@ -217,6 +217,7 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
"expression",
"table",
"d3format",
+ "currency",
"extra",
"warning_text",
]
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 9d6e712a73..7905641f80 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -218,6 +218,7 @@ class DashboardDatasetSchema(Schema):
id = fields.Int()
uid = fields.Str()
column_formats = fields.Dict()
+ currency_formats = fields.Dict()
database = fields.Nested(DatabaseSchema)
default_endpoint = fields.String()
filter_select = fields.Bool()
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 2b6f417e37..87e1d9e74c 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -171,6 +171,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"metrics.changed_on",
"metrics.created_on",
"metrics.d3format",
+ "metrics.currency",
"metrics.description",
"metrics.expression",
"metrics.extra",
@@ -201,6 +202,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"datasource_name",
"name",
"column_formats",
+ "currency_formats",
"granularity_sqla",
"time_grain_sqla",
"order_by_choices",
diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py
index f95897ce59..2b65d674ed 100644
--- a/superset/datasets/schemas.py
+++ b/superset/datasets/schemas.py
@@ -71,6 +71,7 @@ class DatasetMetricsPutSchema(Schema):
metric_name = fields.String(required=True, validate=Length(1, 255))
metric_type = fields.String(allow_none=True, validate=Length(1, 32))
d3format = fields.String(allow_none=True, validate=Length(1, 128))
+ currency = fields.String(allow_none=True, required=False, validate=Length(1, 128))
verbose_name = fields.String(allow_none=True, metadata={Length: (1, 1024)})
warning_text = fields.String(allow_none=True)
uuid = fields.UUID(allow_none=True)
@@ -191,6 +192,7 @@ class ImportV1MetricSchema(Schema):
expression = fields.String(required=True)
description = fields.String(allow_none=True)
d3format = fields.String(allow_none=True)
+ currency = fields.String(allow_none=True, required=False)
extra = fields.Dict(allow_none=True)
warning_text = fields.String(allow_none=True)
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index f4b75c50b1..d117766754 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -158,6 +158,7 @@ class MetricType(TypedDict, total=False):
metric_type: str | None
description: str | None
d3format: str | None
+ currency: str | None
warning_text: str | None
extra: str | None
diff --git a/superset/explore/schemas.py b/superset/explore/schemas.py
index 37044c0394..75c3dcac2c 100644
--- a/superset/explore/schemas.py
+++ b/superset/explore/schemas.py
@@ -25,6 +25,7 @@ class DatasetSchema(Schema):
}
)
column_formats = fields.Dict(metadata={"description": "Column formats."})
+ currency_formats = fields.Dict(metadata={"description": "Currency formats."})
columns = fields.List(fields.Dict(), metadata={"description": "Columns metadata."})
database = fields.Dict(
metadata={"description": "Database associated with the dataset."}
diff --git a/superset/migrations/versions/2023-06-21_14-02_90139bf715e4_add_currency_column_to_metrics.py b/superset/migrations/versions/2023-06-21_14-02_90139bf715e4_add_currency_column_to_metrics.py
new file mode 100644
index 0000000000..7d6f0f2ba0
--- /dev/null
+++ b/superset/migrations/versions/2023-06-21_14-02_90139bf715e4_add_currency_column_to_metrics.py
@@ -0,0 +1,42 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""add_currency_column_to_metrics
+
+Revision ID: 90139bf715e4
+Revises: 83e1abbe777f
+Create Date: 2023-06-21 14:02:08.200955
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "90139bf715e4"
+down_revision = "83e1abbe777f"
+
+import sqlalchemy as sa
+from alembic import op
+
+
+def upgrade():
+ op.add_column("metrics", sa.Column("currency", sa.String(128), nullable=True))
+ op.add_column("sql_metrics", sa.Column("currency", sa.String(128), nullable=True))
+
+
+def downgrade():
+ with op.batch_alter_table("sql_metrics") as batch_op_sql_metrics:
+ batch_op_sql_metrics.drop_column("currency")
+ with op.batch_alter_table("metrics") as batch_op_metrics:
+ batch_op_metrics.drop_column("currency")
diff --git a/superset/views/base.py b/superset/views/base.py
index e66fea0a48..717efdff84 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -430,6 +430,7 @@ def cached_common_bootstrap_data(user: User) -> dict[str, Any]:
"locale": locale,
"language_pack": get_language_pack(locale),
"d3_format": conf.get("D3_FORMAT"),
+ "currencies": conf.get("CURRENCIES"),
"feature_flags": get_feature_flags(),
"extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],
"extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"],
diff --git a/tests/integration_tests/datasets/commands_tests.py b/tests/integration_tests/datasets/commands_tests.py
index 34a0625b36..4919b8886d 100644
--- a/tests/integration_tests/datasets/commands_tests.py
+++ b/tests/integration_tests/datasets/commands_tests.py
@@ -148,6 +148,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
"main_dttm_col": None,
"metrics": [
{
+ "currency": None,
"d3format": None,
"description": None,
"expression": "COUNT(*)",
@@ -158,6 +159,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
"warning_text": None,
},
{
+ "currency": None,
"d3format": None,
"description": None,
"expression": "SUM(value)",
@@ -381,6 +383,7 @@ class TestImportDatasetsCommand(SupersetTestCase):
assert metric.expression == "count(1)"
assert metric.description is None
assert metric.d3format is None
+ assert metric.currency is None
assert metric.extra == "{}"
assert metric.warning_text is None
diff --git a/tests/unit_tests/datasets/commands/export_test.py b/tests/unit_tests/datasets/commands/export_test.py
index c3ad4f764c..17913c2ca4 100644
--- a/tests/unit_tests/datasets/commands/export_test.py
+++ b/tests/unit_tests/datasets/commands/export_test.py
@@ -116,6 +116,7 @@ metrics:
expression: COUNT(*)
description: null
d3format: null
+ currency: null
extra:
warning_markdown: null
warning_text: null