Incorporated the feedback
Signed-off-by: bnechyporenko <bnechyporenko@bol.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': minor
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
Provide the ability to change the base currency from USD to any other currency in cost insights plugin
|
||||
|
||||
@@ -227,7 +227,7 @@ export type ChartData = {
|
||||
|
||||
// @public (undocumented)
|
||||
export type ConfigContextProps = {
|
||||
baseCurrency: string;
|
||||
baseCurrency: Intl.NumberFormat;
|
||||
metrics: Metric[];
|
||||
products: Product[];
|
||||
icons: Icon[];
|
||||
|
||||
Vendored
+1
-1
@@ -24,7 +24,7 @@ export interface Config {
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseCurrency?: string;
|
||||
baseCurrency?: Intl.NumberFormat;
|
||||
|
||||
products?: {
|
||||
[kind: string]: {
|
||||
|
||||
@@ -43,14 +43,12 @@ import { isInvalid, titleOf, tooltipItemOf } from '../../utils/graphs';
|
||||
import { TooltipRenderer } from '../../types';
|
||||
import { useConfig } from '../../hooks';
|
||||
|
||||
const defaultTooltip = (baseCurrency: string) => {
|
||||
const defaultTooltip = (baseCurrency: Intl.NumberFormat) => {
|
||||
const tooltip: TooltipRenderer = ({ label, payload = [] }) => {
|
||||
if (isInvalid({ label, payload })) return null;
|
||||
|
||||
const title = titleOf(label);
|
||||
const items = payload
|
||||
.map(p => tooltipItemOf(baseCurrency, p))
|
||||
.filter(notEmpty);
|
||||
const items = payload.map(tooltipItemOf(baseCurrency)).filter(notEmpty);
|
||||
return (
|
||||
<BarChartTooltip title={title}>
|
||||
{items.map((item, index) => (
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { costFormatter, formatChange } from '../../utils/formatters';
|
||||
import { formatChange } from '../../utils/formatters';
|
||||
import { useEntityDialogStyles as useStyles } from '../../utils/styles';
|
||||
import { CostGrowthIndicator } from '../CostGrowth';
|
||||
import { BarChartOptions, ChangeStatistic, Entity } from '../../types';
|
||||
@@ -36,40 +36,38 @@ type RowData = {
|
||||
change: ChangeStatistic;
|
||||
};
|
||||
|
||||
function createRenderer(
|
||||
baseCurrency: string,
|
||||
col: keyof RowData,
|
||||
classes: Record<string, string>,
|
||||
) {
|
||||
return function render(rowData: {}): JSX.Element {
|
||||
const row = rowData as RowData;
|
||||
const rowStyles = classnames(classes.row, {
|
||||
[classes.rowTotal]: row.id === 'total',
|
||||
[classes.colFirst]: col === 'label',
|
||||
[classes.colLast]: col === 'change',
|
||||
});
|
||||
const createRenderer =
|
||||
(baseCurrency: Intl.NumberFormat) =>
|
||||
(col: keyof RowData, classes: Record<string, string>) => {
|
||||
return function render(rowData: {}): JSX.Element {
|
||||
const row = rowData as RowData;
|
||||
const rowStyles = classnames(classes.row, {
|
||||
[classes.rowTotal]: row.id === 'total',
|
||||
[classes.colFirst]: col === 'label',
|
||||
[classes.colLast]: col === 'change',
|
||||
});
|
||||
|
||||
switch (col) {
|
||||
case 'previous':
|
||||
case 'current':
|
||||
return (
|
||||
<Typography className={rowStyles}>
|
||||
{costFormatter(baseCurrency).format(row[col])}
|
||||
</Typography>
|
||||
);
|
||||
case 'change':
|
||||
return (
|
||||
<CostGrowthIndicator
|
||||
className={rowStyles}
|
||||
change={row.change}
|
||||
formatter={formatChange}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Typography className={rowStyles}>{row.label}</Typography>;
|
||||
}
|
||||
switch (col) {
|
||||
case 'previous':
|
||||
case 'current':
|
||||
return (
|
||||
<Typography className={rowStyles}>
|
||||
{baseCurrency.format(row[col])}
|
||||
</Typography>
|
||||
);
|
||||
case 'change':
|
||||
return (
|
||||
<CostGrowthIndicator
|
||||
className={rowStyles}
|
||||
change={row.change}
|
||||
formatter={formatChange}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Typography className={rowStyles}>{row.label}</Typography>;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// material-table does not support fixed rows. Override the sorting algorithm
|
||||
// to force Total row to bottom by default or when a user sort toggles a column.
|
||||
@@ -122,7 +120,7 @@ export const ProductEntityTable = ({
|
||||
{
|
||||
field: 'label',
|
||||
title: <Typography className={firstColClasses}>{entityLabel}</Typography>,
|
||||
render: createRenderer(baseCurrency, 'label', classes),
|
||||
render: createRenderer(baseCurrency)('label', classes),
|
||||
customSort: createSorter('label'),
|
||||
width: '33.33%',
|
||||
},
|
||||
@@ -132,7 +130,7 @@ export const ProductEntityTable = ({
|
||||
<Typography className={classes.column}>{data.previousName}</Typography>
|
||||
),
|
||||
align: 'right',
|
||||
render: createRenderer(baseCurrency, 'previous', classes),
|
||||
render: createRenderer(baseCurrency)('previous', classes),
|
||||
customSort: createSorter('previous'),
|
||||
},
|
||||
{
|
||||
@@ -141,14 +139,14 @@ export const ProductEntityTable = ({
|
||||
<Typography className={classes.column}>{data.currentName}</Typography>
|
||||
),
|
||||
align: 'right',
|
||||
render: createRenderer(baseCurrency, 'current', classes),
|
||||
render: createRenderer(baseCurrency)('current', classes),
|
||||
customSort: createSorter('current'),
|
||||
},
|
||||
{
|
||||
field: 'change',
|
||||
title: <Typography className={lastColClasses}>Change</Typography>,
|
||||
align: 'right',
|
||||
render: createRenderer(baseCurrency, 'change', classes),
|
||||
render: createRenderer(baseCurrency)('change', classes),
|
||||
customSort: createSorter('change'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -133,9 +133,7 @@ export const ProductInsightsChart = ({
|
||||
const id = label === '' ? null : label;
|
||||
|
||||
const title = titleOf(label);
|
||||
const items = payload
|
||||
.map(p => tooltipItemOf(baseCurrency, p))
|
||||
.filter(notEmpty);
|
||||
const items = payload.map(tooltipItemOf(baseCurrency)).filter(notEmpty);
|
||||
|
||||
const activeEntity = findAlways(entities, e => e.id === id);
|
||||
const breakdowns = Object.keys(activeEntity.entities);
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Config as BackstageConfig } from '@backstage/config';
|
||||
import { Currency, Icon, Metric, Product } from '../types';
|
||||
import { getIcon } from '../utils/navigation';
|
||||
import { validateCurrencies, validateMetrics } from '../utils/config';
|
||||
import { defaultCurrencies } from '../utils/currency';
|
||||
import { createCurrency, defaultCurrencies } from '../utils/currency';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
/*
|
||||
@@ -46,7 +46,13 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
* default: true
|
||||
* metricB:
|
||||
* name: Metric B
|
||||
* baseCurrency: 'EUR'
|
||||
* baseCurrency:
|
||||
* locales:
|
||||
* - en-US
|
||||
* options:
|
||||
* style: currency
|
||||
* currency: EUR
|
||||
* minimumFractionDigits: 3
|
||||
* currencies:
|
||||
* currencyA:
|
||||
* label: Currency A
|
||||
@@ -61,7 +67,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export type ConfigContextProps = {
|
||||
baseCurrency: string;
|
||||
baseCurrency: Intl.NumberFormat;
|
||||
metrics: Metric[];
|
||||
products: Product[];
|
||||
icons: Icon[];
|
||||
@@ -74,7 +80,7 @@ export const ConfigContext = createContext<ConfigContextProps | undefined>(
|
||||
);
|
||||
|
||||
const defaultState: ConfigContextProps = {
|
||||
baseCurrency: 'USD',
|
||||
baseCurrency: createCurrency(),
|
||||
metrics: [],
|
||||
products: [],
|
||||
icons: [],
|
||||
@@ -113,10 +119,37 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
function getBaseCurrency(): string {
|
||||
const baseCurrency = c.getOptionalString('costInsights.baseCurrency');
|
||||
function getBaseCurrency(): Intl.NumberFormat {
|
||||
const baseCurrency = c.getOptionalConfig('costInsights.baseCurrency');
|
||||
if (baseCurrency) {
|
||||
return baseCurrency;
|
||||
const options = baseCurrency.getOptionalConfig('options');
|
||||
return new Intl.NumberFormat(
|
||||
baseCurrency.getOptionalString('locales'),
|
||||
options
|
||||
? {
|
||||
localeMatcher: options.getOptionalString('localeMatcher'),
|
||||
style: options.getOptionalString('style'),
|
||||
currency: options.getOptionalString('currency'),
|
||||
currencySign: options.getOptionalString('currencySign'),
|
||||
useGrouping: options.getOptionalBoolean('useGrouping'),
|
||||
minimumIntegerDigits: options.getOptionalNumber(
|
||||
'minimumIntegerDigits',
|
||||
),
|
||||
minimumFractionDigits: options.getOptionalNumber(
|
||||
'minimumFractionDigits',
|
||||
),
|
||||
maximumFractionDigits: options.getOptionalNumber(
|
||||
'maximumFractionDigits',
|
||||
),
|
||||
minimumSignificantDigits: options.getOptionalNumber(
|
||||
'minimumSignificantDigits',
|
||||
),
|
||||
maximumSignificantDigits: options.getOptionalNumber(
|
||||
'maximumSignificantDigits',
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
return defaultState.baseCurrency;
|
||||
|
||||
@@ -22,9 +22,8 @@ import { ConfigContext, ConfigContextProps } from '../hooks';
|
||||
import { CurrencyContext, CurrencyContextProps } from '../hooks';
|
||||
import { BillingDateContext, BillingDateContextProps } from '../hooks';
|
||||
import { ScrollContext, ScrollContextProps } from '../hooks';
|
||||
import { Group, Duration } from '../types';
|
||||
|
||||
export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }];
|
||||
import { Duration } from '../types';
|
||||
import { createCurrency } from '../utils/currency';
|
||||
|
||||
export type MockFilterProviderProps = PropsWithChildren<
|
||||
Partial<FilterContextProps>
|
||||
@@ -82,7 +81,7 @@ export const MockConfigProvider = (props: MockConfigProviderProps) => {
|
||||
const { children, ...context } = props;
|
||||
|
||||
const defaultContext: ConfigContextProps = {
|
||||
baseCurrency: 'USD',
|
||||
baseCurrency: createCurrency(),
|
||||
metrics: [],
|
||||
products: [],
|
||||
icons: [],
|
||||
|
||||
@@ -61,3 +61,12 @@ export const defaultCurrencies: Currency[] = [
|
||||
rate: 5.5,
|
||||
},
|
||||
];
|
||||
|
||||
export const createCurrency = (
|
||||
currency: string = 'USD',
|
||||
locale: string = 'en-US',
|
||||
) =>
|
||||
new Intl.NumberFormat(locale, {
|
||||
currency,
|
||||
style: 'currency',
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
quarterOf,
|
||||
} from './formatters';
|
||||
import { Duration } from '../types';
|
||||
import { createCurrency } from './currency';
|
||||
|
||||
Date.now = jest.fn(() => new Date(Date.parse('2019-12-07')).valueOf());
|
||||
|
||||
@@ -39,7 +40,7 @@ describe('date formatters', () => {
|
||||
0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566,
|
||||
];
|
||||
const formattedValues = values.map(val =>
|
||||
lengthyCurrencyFormatter('USD').format(val),
|
||||
lengthyCurrencyFormatter(createCurrency()).format(val),
|
||||
);
|
||||
expect(formattedValues).toEqual([
|
||||
'$0.00000041',
|
||||
@@ -55,7 +56,7 @@ describe('date formatters', () => {
|
||||
0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566,
|
||||
];
|
||||
const formattedValues = values.map(val =>
|
||||
lengthyCurrencyFormatter('EUR').format(val),
|
||||
lengthyCurrencyFormatter(createCurrency('EUR')).format(val),
|
||||
);
|
||||
expect(formattedValues).toEqual([
|
||||
'€0.00000041',
|
||||
|
||||
@@ -25,28 +25,28 @@ export type Period = {
|
||||
periodEnd: string;
|
||||
};
|
||||
|
||||
export const costFormatter = (currency: string) =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
});
|
||||
export const currencyFormatter = (currency: Intl.NumberFormat) => {
|
||||
const options = currency.resolvedOptions();
|
||||
|
||||
export const currencyFormatter = (currency: string) =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
return new Intl.NumberFormat(options.locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
currency: options.currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
};
|
||||
|
||||
export const lengthyCurrencyFormatter = (currency: string) =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
export const lengthyCurrencyFormatter = (currency: Intl.NumberFormat) => {
|
||||
const options = currency.resolvedOptions();
|
||||
|
||||
return new Intl.NumberFormat(options.locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
currency: options.currency,
|
||||
minimumFractionDigits: 0,
|
||||
minimumSignificantDigits: 2,
|
||||
maximumSignificantDigits: 2,
|
||||
});
|
||||
};
|
||||
|
||||
export const numberFormatter = new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: 0,
|
||||
|
||||
@@ -16,16 +16,19 @@
|
||||
|
||||
import { formatGraphValue, tooltipItemOf } from './graphs';
|
||||
import { DataKey } from '../types';
|
||||
import { createCurrency } from './currency';
|
||||
|
||||
describe('graphs', () => {
|
||||
it('formatGraphValue', () => {
|
||||
expect(formatGraphValue('SEK')(1000, 0)).toEqual('SEK 1,000');
|
||||
expect(formatGraphValue('EUR')(1000, 0)).toEqual('€1,000');
|
||||
expect(formatGraphValue('USD')(1000, 0)).toEqual('$1,000');
|
||||
expect(formatGraphValue(createCurrency('SEK'))(1000, 0)).toEqual(
|
||||
'SEK 1,000',
|
||||
);
|
||||
expect(formatGraphValue(createCurrency('EUR'))(1000, 0)).toEqual('€1,000');
|
||||
expect(formatGraphValue(createCurrency('USD'))(1000, 0)).toEqual('$1,000');
|
||||
});
|
||||
it('tooltipItemOf', () => {
|
||||
expect(
|
||||
tooltipItemOf('EUR', {
|
||||
tooltipItemOf(createCurrency('EUR'))({
|
||||
value: '1000',
|
||||
color: 'red',
|
||||
dataKey: DataKey.Current,
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from './formatters';
|
||||
|
||||
export const formatGraphValue =
|
||||
(baseCurrency: string) =>
|
||||
(baseCurrency: Intl.NumberFormat) =>
|
||||
(value: number, _index: number, format?: string) => {
|
||||
if (format === 'number') {
|
||||
return value.toLocaleString();
|
||||
@@ -40,28 +40,26 @@ export const formatGraphValue =
|
||||
export const overviewGraphTickFormatter = (millis: string | number) =>
|
||||
typeof millis === 'number' ? dateFormatter.format(millis) : millis;
|
||||
|
||||
export const tooltipItemOf = (
|
||||
baseCurrency: string,
|
||||
payload: Payload<string, string>,
|
||||
) => {
|
||||
const value =
|
||||
payload.value && !isNaN(Number(payload.value))
|
||||
? currencyFormatter(baseCurrency).format(Number(payload.value))
|
||||
: payload.value;
|
||||
const fill = payload.color as string;
|
||||
export const tooltipItemOf =
|
||||
(baseCurrency: Intl.NumberFormat) => (payload: Payload<string, string>) => {
|
||||
const value =
|
||||
payload.value && !isNaN(Number(payload.value))
|
||||
? baseCurrency.format(Number(payload.value))
|
||||
: payload.value;
|
||||
const fill = payload.color as string;
|
||||
|
||||
switch (payload.dataKey) {
|
||||
case DataKey.Current:
|
||||
case DataKey.Previous:
|
||||
return {
|
||||
label: payload.name,
|
||||
value: value,
|
||||
fill: fill,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
switch (payload.dataKey) {
|
||||
case DataKey.Current:
|
||||
case DataKey.Previous:
|
||||
return {
|
||||
label: payload.name,
|
||||
value: value,
|
||||
fill: fill,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const resourceOf = (entity: Entity | AlertCost): ResourceData => ({
|
||||
name: entity.id,
|
||||
|
||||
Reference in New Issue
Block a user