From 5c1bfac3453c38ff0f47ae98fe7cfe94d77ad232 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Thu, 17 Nov 2022 15:12:26 +0100 Subject: [PATCH] Incorporated the feedback Signed-off-by: bnechyporenko --- .changeset/four-adults-provide.md | 2 +- plugins/cost-insights/api-report.md | 2 +- plugins/cost-insights/config.d.ts | 2 +- .../src/components/BarChart/BarChart.tsx | 6 +- .../ProductEntityTable.tsx | 72 +++++++++---------- .../ProductInsightsChart.tsx | 4 +- plugins/cost-insights/src/hooks/useConfig.tsx | 47 ++++++++++-- .../cost-insights/src/testUtils/providers.tsx | 7 +- plugins/cost-insights/src/utils/currency.ts | 9 +++ .../src/utils/formatters.test.ts | 5 +- plugins/cost-insights/src/utils/formatters.ts | 22 +++--- .../cost-insights/src/utils/graphs.test.ts | 11 +-- plugins/cost-insights/src/utils/graphs.ts | 42 ++++++----- 13 files changed, 134 insertions(+), 97 deletions(-) diff --git a/.changeset/four-adults-provide.md b/.changeset/four-adults-provide.md index f6f24bca75..0edb4807d9 100644 --- a/.changeset/four-adults-provide.md +++ b/.changeset/four-adults-provide.md @@ -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 diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md index 5906c952db..b4c818f1e4 100644 --- a/plugins/cost-insights/api-report.md +++ b/plugins/cost-insights/api-report.md @@ -227,7 +227,7 @@ export type ChartData = { // @public (undocumented) export type ConfigContextProps = { - baseCurrency: string; + baseCurrency: Intl.NumberFormat; metrics: Metric[]; products: Product[]; icons: Icon[]; diff --git a/plugins/cost-insights/config.d.ts b/plugins/cost-insights/config.d.ts index 7c725eef8c..7c8b9d265d 100644 --- a/plugins/cost-insights/config.d.ts +++ b/plugins/cost-insights/config.d.ts @@ -24,7 +24,7 @@ export interface Config { /** * @visibility frontend */ - baseCurrency?: string; + baseCurrency?: Intl.NumberFormat; products?: { [kind: string]: { diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.tsx index fa97e640bd..fa4ae18e24 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.tsx @@ -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 ( {items.map((item, index) => ( diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx index 0d943d66a5..c20c7d4a1f 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx @@ -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, -) { - 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) => { + 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 ( - - {costFormatter(baseCurrency).format(row[col])} - - ); - case 'change': - return ( - - ); - default: - return {row.label}; - } + switch (col) { + case 'previous': + case 'current': + return ( + + {baseCurrency.format(row[col])} + + ); + case 'change': + return ( + + ); + default: + return {row.label}; + } + }; }; -} // 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: {entityLabel}, - render: createRenderer(baseCurrency, 'label', classes), + render: createRenderer(baseCurrency)('label', classes), customSort: createSorter('label'), width: '33.33%', }, @@ -132,7 +130,7 @@ export const ProductEntityTable = ({ {data.previousName} ), align: 'right', - render: createRenderer(baseCurrency, 'previous', classes), + render: createRenderer(baseCurrency)('previous', classes), customSort: createSorter('previous'), }, { @@ -141,14 +139,14 @@ export const ProductEntityTable = ({ {data.currentName} ), align: 'right', - render: createRenderer(baseCurrency, 'current', classes), + render: createRenderer(baseCurrency)('current', classes), customSort: createSorter('current'), }, { field: 'change', title: Change, align: 'right', - render: createRenderer(baseCurrency, 'change', classes), + render: createRenderer(baseCurrency)('change', classes), customSort: createSorter('change'), }, ]; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx index cc11eb40b5..f44e24f443 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -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); diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index ce2681844e..34ecfd463d 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -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( ); 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; diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 764e90d547..761a908ca4 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -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 @@ -82,7 +81,7 @@ export const MockConfigProvider = (props: MockConfigProviderProps) => { const { children, ...context } = props; const defaultContext: ConfigContextProps = { - baseCurrency: 'USD', + baseCurrency: createCurrency(), metrics: [], products: [], icons: [], diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts index 60115aad3a..400d3bcb72 100644 --- a/plugins/cost-insights/src/utils/currency.ts +++ b/plugins/cost-insights/src/utils/currency.ts @@ -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', + }); diff --git a/plugins/cost-insights/src/utils/formatters.test.ts b/plugins/cost-insights/src/utils/formatters.test.ts index 0bc95c2efe..f1cf778b6c 100644 --- a/plugins/cost-insights/src/utils/formatters.test.ts +++ b/plugins/cost-insights/src/utils/formatters.test.ts @@ -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', diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 4258b1bfce..fd3fb4c0ef 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -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, diff --git a/plugins/cost-insights/src/utils/graphs.test.ts b/plugins/cost-insights/src/utils/graphs.test.ts index 47e7cae752..5b425edb44 100644 --- a/plugins/cost-insights/src/utils/graphs.test.ts +++ b/plugins/cost-insights/src/utils/graphs.test.ts @@ -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, diff --git a/plugins/cost-insights/src/utils/graphs.ts b/plugins/cost-insights/src/utils/graphs.ts index 8f82d41054..896d09f865 100644 --- a/plugins/cost-insights/src/utils/graphs.ts +++ b/plugins/cost-insights/src/utils/graphs.ts @@ -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, -) => { - 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) => { + 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,