Incorporated the feedback

Signed-off-by: bnechyporenko <bnechyporenko@bol.com>
This commit is contained in:
bnechyporenko
2022-11-17 15:12:26 +01:00
parent f7475ecc86
commit 5c1bfac345
13 changed files with 134 additions and 97 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -227,7 +227,7 @@ export type ChartData = {
// @public (undocumented)
export type ConfigContextProps = {
baseCurrency: string;
baseCurrency: Intl.NumberFormat;
metrics: Metric[];
products: Product[];
icons: Icon[];
+1 -1
View File
@@ -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);
+40 -7
View File
@@ -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',
+11 -11
View File
@@ -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,
+20 -22
View File
@@ -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,