mirror of https://github.com/apache/superset.git
feat: Implement currencies formatter for saved metrics (#24517)
This commit is contained in:
parent
e402c94a9f
commit
83ff4cd86a
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]']
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -40,6 +40,7 @@ describe('defineSavedMetrics', () => {
|
|||
columns: [],
|
||||
verbose_map: {},
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
datasource_name: 'my_datasource',
|
||||
description: 'this is my datasource',
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default as CurrencyFormatter } from './CurrencyFormatter';
|
||||
export * from './CurrencyFormatter';
|
|
@ -36,3 +36,4 @@ export * from './components';
|
|||
export * from './math-expression';
|
||||
export * from './ui-overrides';
|
||||
export * from './hooks';
|
||||
export * from './currency-format';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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>`);
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
} = {},
|
||||
|
|
|
@ -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);
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('PivotTableChart buildQuery', () => {
|
|||
combineMetric: false,
|
||||
verboseMap: {},
|
||||
columnFormats: {},
|
||||
currencyFormats: {},
|
||||
metricColorFormatters: [],
|
||||
dateFormatters: {},
|
||||
setDataMask: () => {},
|
||||
|
|
|
@ -90,6 +90,7 @@ describe('PivotTableChart transformProps', () => {
|
|||
dateFormatters: {},
|
||||
emitCrossFilters: false,
|
||||
columnFormats: {},
|
||||
currencyFormats: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-table#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -29,6 +29,7 @@ export const PLACEHOLDER_DATASOURCE: Datasource = {
|
|||
column_types: [],
|
||||
metrics: [],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
description: '',
|
||||
|
|
|
@ -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]']
|
||||
|
|
|
@ -96,6 +96,7 @@ export const hydrateExplore =
|
|||
if (dashboardId) {
|
||||
initialFormData.dashboardId = dashboardId;
|
||||
}
|
||||
|
||||
const initialDatasource = dataset;
|
||||
|
||||
const initialExploreState = {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ const TEST_DATASOURCE = {
|
|||
columns: [],
|
||||
metrics: [],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '__timestamp',
|
||||
// eg. ['["ds", true]', 'ds [asc]']
|
||||
|
|
|
@ -374,6 +374,8 @@ class D3Format(TypedDict, total=False):
|
|||
|
||||
D3_FORMAT: D3Format = {}
|
||||
|
||||
CURRENCIES = ["USD", "EUR", "GBP", "INR", "MXN", "JPY", "CNY"]
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Feature flags
|
||||
# ---------------------------------------------------
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -416,6 +416,7 @@ class SqlMetric(Model, BaseMetric, CertificationMixin):
|
|||
"expression",
|
||||
"description",
|
||||
"d3format",
|
||||
"currency",
|
||||
"extra",
|
||||
"warning_text",
|
||||
]
|
||||
|
|
|
@ -217,6 +217,7 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
|
|||
"expression",
|
||||
"table",
|
||||
"d3format",
|
||||
"currency",
|
||||
"extra",
|
||||
"warning_text",
|
||||
]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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."}
|
||||
|
|
|
@ -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")
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -116,6 +116,7 @@ metrics:
|
|||
expression: COUNT(*)
|
||||
description: null
|
||||
d3format: null
|
||||
currency: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
warning_text: null
|
||||
|
|
Loading…
Reference in New Issue