From cadb2f3eaf3ff786c82f6286138d21daf2262811 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 15 Nov 2022 13:35:12 +0100 Subject: [PATCH 01/14] Cost Insights Configurable Default Currency Signed-off-by: bnechyporenko --- app-config.yaml | 1 + plugins/cost-insights/config.d.ts | 5 +++ .../src/components/BarChart/BarChart.test.tsx | 14 ++++--- .../src/components/BarChart/BarChart.tsx | 36 ++++++++++------- .../BarChart/BarChartLegend.test.tsx | 5 ++- .../components/BarChart/BarChartLegend.tsx | 6 ++- .../CostOverviewBreakdownChart.tsx | 29 +++++++------- .../CostOverviewCard/CostOverviewChart.tsx | 18 +++++++-- .../ProductEntityDialog.test.tsx | 25 +++++++----- .../ProductEntityTable.tsx | 18 ++++++--- .../ProductInsightsChart.tsx | 8 +++- .../UnlabeledDataflowAlertCard.test.tsx | 17 +++++--- plugins/cost-insights/src/hooks/useConfig.tsx | 14 +++++++ .../cost-insights/src/testUtils/providers.tsx | 18 ++++----- .../src/utils/formatters.test.ts | 2 +- plugins/cost-insights/src/utils/formatters.ts | 39 ++++++++++--------- plugins/cost-insights/src/utils/graphs.ts | 31 ++++++++------- 17 files changed, 177 insertions(+), 109 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index ef133491af..7064d406f7 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -372,6 +372,7 @@ auth: myproxy: development: {} costInsights: + baseCurrency: 'EUR' engineerCost: 200000 products: computeEngine: diff --git a/plugins/cost-insights/config.d.ts b/plugins/cost-insights/config.d.ts index 4144b6d6a5..7c725eef8c 100644 --- a/plugins/cost-insights/config.d.ts +++ b/plugins/cost-insights/config.d.ts @@ -21,6 +21,11 @@ export interface Config { */ engineerCost: number; + /** + * @visibility frontend + */ + baseCurrency?: string; + products?: { [kind: string]: { /** diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx index 4e3ca9d78c..530ca16e8f 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { BarChart, BarChartProps } from './BarChart'; import { ResourceData } from '../../types'; -import { createMockEntity } from '../../testUtils'; +import { createMockEntity, MockConfigProvider } from '../../testUtils'; import { resourceSort } from '../../utils/sort'; import { renderInTestApp } from '@backstage/test-utils'; @@ -46,11 +46,13 @@ const renderWithProps = ({ resources = MockResources, }: BarChartProps) => { return renderInTestApp( - , + + + , ); }; diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.tsx index 4f4dea8ddd..fa97e640bd 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.tsx @@ -40,20 +40,26 @@ import { notEmpty } from '../../utils/assert'; import { useBarChartStyles } from '../../utils/styles'; import { resourceSort } from '../../utils/sort'; import { isInvalid, titleOf, tooltipItemOf } from '../../utils/graphs'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { TooltipRenderer } from '../../types'; +import { useConfig } from '../../hooks'; -export const defaultTooltip: TooltipRenderer = ({ label, payload = [] }) => { - if (isInvalid({ label, payload })) return null; +const defaultTooltip = (baseCurrency: string) => { + const tooltip: TooltipRenderer = ({ label, payload = [] }) => { + if (isInvalid({ label, payload })) return null; - const title = titleOf(label); - const items = payload.map(tooltipItemOf).filter(notEmpty); - return ( - - {items.map((item, index) => ( - - ))} - - ); + const title = titleOf(label); + const items = payload + .map(p => tooltipItemOf(baseCurrency, p)) + .filter(notEmpty); + return ( + + {items.map((item, index) => ( + + ))} + + ); + }; + return tooltip; }; /** @public */ @@ -69,12 +75,14 @@ export type BarChartProps = { /** @public */ export const BarChart = (props: BarChartProps) => { + const { baseCurrency } = useConfig(); + const { resources, responsive = true, displayAmount = 6, options = {}, - tooltip = defaultTooltip, + tooltip = defaultTooltip(baseCurrency), onClick, onMouseMove, } = props; @@ -164,7 +172,7 @@ export const BarChart = (props: BarChartProps) => { tick={BarChartTick} /> 0, globalResourcesMax]} tick={styles.axis} /> diff --git a/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx b/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx index 07ff144862..690f0bbcd8 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx @@ -17,11 +17,14 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { BarChartLegend } from './BarChartLegend'; +import { MockConfigProvider } from '../../testUtils'; describe('', () => { it(`Should display the correct cost start and end`, async () => { const rendered = await renderInTestApp( - , + + , + , ); expect(rendered.getByText(/\$1,000/)).toBeInTheDocument(); expect(rendered.queryByText(/\$5,000/)).toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx b/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx index 72c3120dd4..becd5467d1 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx @@ -20,6 +20,7 @@ import { LegendItem } from '../LegendItem'; import { currencyFormatter } from '../../utils/formatters'; import { CostInsightsTheme } from '../../types'; import { useBarChartLayoutStyles as useStyles } from '../../utils/styles'; +import { useConfig } from '../../hooks'; /** @public */ export type BarChartLegendOptions = { @@ -45,6 +46,7 @@ export const BarChartLegend = ( const theme = useTheme(); const classes = useStyles(); + const { baseCurrency } = useConfig(); const data = Object.assign( { @@ -63,7 +65,7 @@ export const BarChartLegend = ( title={data.previousName} markerColor={options.hideMarker ? undefined : data.previousFill} > - {currencyFormatter.format(costStart)} + {currencyFormatter(baseCurrency).format(costStart)} @@ -71,7 +73,7 @@ export const BarChartLegend = ( title={data.currentName} markerColor={options.hideMarker ? undefined : data.currentFill} > - {currencyFormatter.format(costEnd)} + {currencyFormatter(baseCurrency).format(costEnd)} {children} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx index 962dc6b332..838da0d1f4 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx @@ -16,41 +16,41 @@ import React, { useState } from 'react'; import { DateTime } from 'luxon'; import { - useTheme, Box, - Typography, Divider, emphasize, + Typography, + useTheme, } from '@material-ui/core'; import { default as FullScreenIcon } from '@material-ui/icons/Fullscreen'; import { + Area, AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip as RechartsTooltip, XAxis, YAxis, - Tooltip as RechartsTooltip, - Area, - ResponsiveContainer, - CartesianGrid, } from 'recharts'; -import { Cost, DEFAULT_DATE_FORMAT, CostInsightsTheme } from '../../types'; +import { Cost, CostInsightsTheme, DEFAULT_DATE_FORMAT } from '../../types'; import { + BarChartLegend, BarChartTooltip as Tooltip, BarChartTooltipItem as TooltipItem, - BarChartLegend, } from '../BarChart'; import { - overviewGraphTickFormatter, formatGraphValue, isInvalid, + overviewGraphTickFormatter, } from '../../utils/graphs'; import { useCostOverviewStyles as useStyles } from '../../utils/styles'; -import { useFilters, useLastCompleteBillingDate } from '../../hooks'; +import { useConfig, useFilters, useLastCompleteBillingDate } from '../../hooks'; import { mapFiltersToProps } from './selector'; import { getPreviousPeriodTotalCost } from '../../utils/change'; import { formatPeriod } from '../../utils/formatters'; import { aggregationSum } from '../../utils/sum'; -import { BarChartLegendOptions } from '../BarChart/BarChartLegend'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { BarChartLegendOptions } from '../BarChart'; +import { TooltipRenderer } from '../../types'; export type CostOverviewBreakdownChartProps = { costBreakdown: Cost[]; @@ -65,6 +65,7 @@ export const CostOverviewBreakdownChart = ({ }: CostOverviewBreakdownChartProps) => { const theme = useTheme(); const classes = useStyles(theme); + const { baseCurrency } = useConfig(); const lastCompleteBillingDate = useLastCompleteBillingDate(); const { duration } = useFilters(mapFiltersToProps); const [isExpanded, setExpanded] = useState(false); @@ -187,7 +188,7 @@ export const CostOverviewBreakdownChart = ({ const dateTitle = date.toUTC().toFormat(DEFAULT_DATE_FORMAT); const items = payload.map((p, i) => ({ label: p.dataKey as string, - value: formatGraphValue(Number(p.value), i), + value: formatGraphValue(baseCurrency)(Number(p.value), i), fill: p.color!, })); const expandText = ( @@ -253,7 +254,7 @@ export const CostOverviewBreakdownChart = ({ 0, 'dataMax']} tick={{ fill: classes.axis.fill }} - tickFormatter={formatGraphValue} + tickFormatter={formatGraphValue(baseCurrency)} width={classes.yAxis.width} /> {renderAreas()} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx index 1c58bf58e6..4232c56ffc 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -48,7 +48,8 @@ import { useCostOverviewStyles as useStyles } from '../../utils/styles'; import { groupByDate, toDataMax, trendFrom } from '../../utils/charts'; import { aggregationSort } from '../../utils/sort'; import { CostOverviewLegend } from './CostOverviewLegend'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { TooltipRenderer } from '../../types'; +import { useConfig } from '../../hooks'; type CostOverviewChartProps = { metric: Maybe; @@ -65,6 +66,7 @@ export const CostOverviewChart = ({ }: CostOverviewChartProps) => { const theme = useTheme(); const styles = useStyles(theme); + const { baseCurrency } = useConfig(); const data = { dailyCost: { @@ -115,8 +117,16 @@ export const CostOverviewChart = ({ : data.metric.name, value: p.dataKey === data.dailyCost.dataKey - ? formatGraphValue(Number(p.value), i, data.dailyCost.format) - : formatGraphValue(Number(p.value), i, data.metric.format), + ? formatGraphValue(baseCurrency)( + Number(p.value), + i, + data.dailyCost.format, + ) + : formatGraphValue(baseCurrency)( + Number(p.value), + i, + data.metric.format, + ), fill: p.dataKey === data.dailyCost.dataKey ? theme.palette.blue @@ -157,7 +167,7 @@ export const CostOverviewChart = ({ 0, 'dataMax']} tick={{ fill: styles.axis.fill }} - tickFormatter={formatGraphValue} + tickFormatter={formatGraphValue(baseCurrency)} width={styles.yAxis.width} yAxisId={data.dailyCost.dataKey} /> diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx index d3d12abe74..fef201ec12 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx @@ -19,6 +19,7 @@ import { wrapInTestApp } from '@backstage/test-utils'; import { ProductEntityDialog } from './ProductEntityDialog'; import { render } from '@testing-library/react'; import { Entity } from '../../types'; +import { MockConfigProvider } from '../../testUtils'; const atomicEntity: Entity = { id: null, @@ -86,11 +87,13 @@ describe('', () => { it('Should show a tab for a single sub-entity type', () => { const { getByText } = render( wrapInTestApp( - , + + + , ), ); expect(getByText('Breakdown by SKU')).toBeInTheDocument(); @@ -99,11 +102,13 @@ describe('', () => { it('Should show tabs when multiple sub-entity types exist', () => { const { getByText } = render( wrapInTestApp( - , + + + , ), ); expect(getByText('Breakdown by SKU')).toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx index 95ba4f2f2d..0d943d66a5 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx @@ -22,6 +22,7 @@ import { useEntityDialogStyles as useStyles } from '../../utils/styles'; import { CostGrowthIndicator } from '../CostGrowth'; import { BarChartOptions, ChangeStatistic, Entity } from '../../types'; import { Table, TableColumn } from '@backstage/core-components'; +import { useConfig } from '../../hooks'; export type ProductEntityTableOptions = Partial< Pick @@ -35,7 +36,11 @@ type RowData = { change: ChangeStatistic; }; -function createRenderer(col: keyof RowData, classes: Record) { +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, { @@ -49,7 +54,7 @@ function createRenderer(col: keyof RowData, classes: Record) { case 'current': return ( - {costFormatter.format(row[col])} + {costFormatter(baseCurrency).format(row[col])} ); case 'change': @@ -99,6 +104,7 @@ export const ProductEntityTable = ({ options, }: ProductEntityTableProps) => { const classes = useStyles(); + const { baseCurrency } = useConfig(); const entities = entity.entities[entityLabel]; const data = Object.assign( @@ -116,7 +122,7 @@ export const ProductEntityTable = ({ { field: 'label', title: {entityLabel}, - render: createRenderer('label', classes), + render: createRenderer(baseCurrency, 'label', classes), customSort: createSorter('label'), width: '33.33%', }, @@ -126,7 +132,7 @@ export const ProductEntityTable = ({ {data.previousName} ), align: 'right', - render: createRenderer('previous', classes), + render: createRenderer(baseCurrency, 'previous', classes), customSort: createSorter('previous'), }, { @@ -135,14 +141,14 @@ export const ProductEntityTable = ({ {data.currentName} ), align: 'right', - render: createRenderer('current', classes), + render: createRenderer(baseCurrency, 'current', classes), customSort: createSorter('current'), }, { field: 'change', title: Change, align: 'right', - render: createRenderer('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 b77b415e1e..cc11eb40b5 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -50,7 +50,8 @@ import { } from '../../utils/styles'; import { Duration, Entity, Maybe } from '../../types'; import { choose } from '../../utils/change'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { TooltipRenderer } from '../../types'; +import { useConfig } from '../../hooks'; export type ProductInsightsChartProps = { billingDate: string; @@ -65,6 +66,7 @@ export const ProductInsightsChart = ({ }: ProductInsightsChartProps) => { const classes = useStyles(); const layoutClasses = useLayoutStyles(); + const { baseCurrency } = useConfig(); // Only a single entities Record for the root product entity is supported const entities = useMemo(() => { @@ -131,7 +133,9 @@ export const ProductInsightsChart = ({ const id = label === '' ? null : label; const title = titleOf(label); - const items = payload.map(tooltipItemOf).filter(notEmpty); + const items = payload + .map(p => tooltipItemOf(baseCurrency, p)) + .filter(notEmpty); const activeEntity = findAlways(entities, e => e.id === id); const breakdowns = Object.keys(activeEntity.entities); diff --git a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx index 06353535a5..3ef6e54a53 100644 --- a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx +++ b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx @@ -19,6 +19,7 @@ import { UnlabeledDataflowAlertCard } from './UnlabeledDataflowAlertCard'; import { createMockUnlabeledDataflowData, createMockUnlabeledDataflowAlertProject, + MockConfigProvider, } from '../../testUtils'; import { renderInTestApp } from '@backstage/test-utils'; @@ -61,9 +62,11 @@ describe('', () => { 'projects with unlabeled Dataflow jobs in the last 30 days.', ); const rendered = await renderInTestApp( - , + + + , ); expect(rendered.getByText(subheader)).toBeInTheDocument(); }); @@ -71,9 +74,11 @@ describe('', () => { it('renders the correct subheader for a single project', async () => { const subheader = new RegExp('1 project'); const rendered = await renderInTestApp( - , + + + , ); expect(rendered.getByText(subheader)).toBeInTheDocument(); }); diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 322e1e35f1..ce2681844e 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -46,6 +46,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; * default: true * metricB: * name: Metric B + * baseCurrency: 'EUR' * currencies: * currencyA: * label: Currency A @@ -60,6 +61,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; /** @public */ export type ConfigContextProps = { + baseCurrency: string; metrics: Metric[]; products: Product[]; icons: Icon[]; @@ -72,6 +74,7 @@ export const ConfigContext = createContext( ); const defaultState: ConfigContextProps = { + baseCurrency: 'USD', metrics: [], products: [], icons: [], @@ -110,6 +113,15 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { return []; } + function getBaseCurrency(): string { + const baseCurrency = c.getOptionalString('costInsights.baseCurrency'); + if (baseCurrency) { + return baseCurrency; + } + + return defaultState.baseCurrency; + } + function getCurrencies(): Currency[] { const currencies = c.getOptionalConfig('costInsights.currencies'); if (currencies) { @@ -141,6 +153,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { } function getConfig() { + const baseCurrency = getBaseCurrency(); const products = getProducts(); const metrics = getMetrics(); const engineerCost = getEngineerCost(); @@ -152,6 +165,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { setConfig(prevState => ({ ...prevState, + baseCurrency, metrics, products, engineerCost, diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 8088fca3c6..764e90d547 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -15,16 +15,13 @@ */ import React, { PropsWithChildren } from 'react'; -import { LoadingContext, LoadingContextProps } from '../hooks/useLoading'; -import { GroupsContext, GroupsContextProps } from '../hooks/useGroups'; -import { FilterContext, FilterContextProps } from '../hooks/useFilters'; -import { ConfigContext, ConfigContextProps } from '../hooks/useConfig'; -import { CurrencyContext, CurrencyContextProps } from '../hooks/useCurrency'; -import { - BillingDateContext, - BillingDateContextProps, -} from '../hooks/useLastCompleteBillingDate'; -import { ScrollContext, ScrollContextProps } from '../hooks/useScroll'; +import { LoadingContext, LoadingContextProps } from '../hooks'; +import { GroupsContext, GroupsContextProps } from '../hooks'; +import { FilterContext, FilterContextProps } from '../hooks'; +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' }]; @@ -85,6 +82,7 @@ export const MockConfigProvider = (props: MockConfigProviderProps) => { const { children, ...context } = props; const defaultContext: ConfigContextProps = { + baseCurrency: 'USD', metrics: [], products: [], icons: [], diff --git a/plugins/cost-insights/src/utils/formatters.test.ts b/plugins/cost-insights/src/utils/formatters.test.ts index 8bf0a6a9d7..87a2b335d9 100644 --- a/plugins/cost-insights/src/utils/formatters.test.ts +++ b/plugins/cost-insights/src/utils/formatters.test.ts @@ -39,7 +39,7 @@ describe('date formatters', () => { 0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566, ]; const formattedValues = values.map(val => - lengthyCurrencyFormatter.format(val), + lengthyCurrencyFormatter('USD').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 e9b19097a4..4258b1bfce 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -17,7 +17,7 @@ import { DateTime, Duration as LuxonDuration } from 'luxon'; import pluralize from 'pluralize'; import { ChangeStatistic, Duration } from '../types'; -import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; import { notEmpty } from './assert'; export type Period = { @@ -25,25 +25,28 @@ export type Period = { periodEnd: string; }; -export const costFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}); +export const costFormatter = (currency: string) => + new Intl.NumberFormat('en-US', { + style: 'currency', + currency, + }); -export const currencyFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - maximumFractionDigits: 0, -}); +export const currencyFormatter = (currency: string) => + new Intl.NumberFormat('en-US', { + style: 'currency', + currency, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); -export const lengthyCurrencyFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - minimumSignificantDigits: 2, - maximumSignificantDigits: 2, -}); +export const lengthyCurrencyFormatter = (currency: string) => + new Intl.NumberFormat('en-US', { + style: 'currency', + 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.ts b/plugins/cost-insights/src/utils/graphs.ts index 172f6cd7e3..2d63967c82 100644 --- a/plugins/cost-insights/src/utils/graphs.ts +++ b/plugins/cost-insights/src/utils/graphs.ts @@ -23,29 +23,30 @@ import { lengthyCurrencyFormatter, } from './formatters'; -export function formatGraphValue( - value: number, - _index: number, - format?: string, -) { - if (format === 'number') { - return value.toLocaleString(); - } +export const formatGraphValue = + (baseCurrency: string) => + (value: number, _index: number, format?: string) => { + if (format === 'number') { + return value.toLocaleString(); + } - if (value < 1) { - return lengthyCurrencyFormatter.format(value); - } + if (value < 1) { + return lengthyCurrencyFormatter(baseCurrency).format(value); + } - return currencyFormatter.format(value); -} + return currencyFormatter(baseCurrency).format(value); + }; export const overviewGraphTickFormatter = (millis: string | number) => typeof millis === 'number' ? dateFormatter.format(millis) : millis; -export const tooltipItemOf = (payload: Payload) => { +export const tooltipItemOf = ( + baseCurrency: string, + payload: Payload, +) => { const value = typeof payload.value === 'number' - ? currencyFormatter.format(payload.value) + ? currencyFormatter(baseCurrency).format(payload.value) : payload.value; const fill = payload.color as string; From f44b45db143010f411d99b313acdca53ceb37938 Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Tue, 15 Nov 2022 13:40:39 +0100 Subject: [PATCH 02/14] Generated Api Report Signed-off-by: bogdannechyporenko --- plugins/cost-insights/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md index dd4e404450..5906c952db 100644 --- a/plugins/cost-insights/api-report.md +++ b/plugins/cost-insights/api-report.md @@ -227,6 +227,7 @@ export type ChartData = { // @public (undocumented) export type ConfigContextProps = { + baseCurrency: string; metrics: Metric[]; products: Product[]; icons: Icon[]; From f9bbb3be375dbd55cbe807a64bc19dcc1e09496e Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Tue, 15 Nov 2022 13:48:14 +0100 Subject: [PATCH 03/14] Added a changeset Signed-off-by: bogdannechyporenko --- .changeset/four-adults-provide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-adults-provide.md diff --git a/.changeset/four-adults-provide.md b/.changeset/four-adults-provide.md new file mode 100644 index 0000000000..9b9b9e1bef --- /dev/null +++ b/.changeset/four-adults-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +Provide the abilility to change the base currency from USD to any other currency in cost insights plugin From 176cd00ea3a74c093faa9cda31b0a519f2282118 Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Tue, 15 Nov 2022 14:16:51 +0100 Subject: [PATCH 04/14] Added test cases Signed-off-by: bogdannechyporenko --- .../src/utils/formatters.test.ts | 16 ++++++++ .../cost-insights/src/utils/graphs.test.ts | 40 +++++++++++++++++++ plugins/cost-insights/src/utils/graphs.ts | 4 +- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 plugins/cost-insights/src/utils/graphs.test.ts diff --git a/plugins/cost-insights/src/utils/formatters.test.ts b/plugins/cost-insights/src/utils/formatters.test.ts index 87a2b335d9..0bc95c2efe 100644 --- a/plugins/cost-insights/src/utils/formatters.test.ts +++ b/plugins/cost-insights/src/utils/formatters.test.ts @@ -49,6 +49,22 @@ describe('date formatters', () => { '$0.0000023', ]); }); + + it('Correctly formats values in euros to two significant digits', () => { + const values = [ + 0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566, + ]; + const formattedValues = values.map(val => + lengthyCurrencyFormatter('EUR').format(val), + ); + expect(formattedValues).toEqual([ + '€0.00000041', + '€0.21', + '€0.00000040', + '€0.41', + '€0.0000023', + ]); + }); }); describe.each` diff --git a/plugins/cost-insights/src/utils/graphs.test.ts b/plugins/cost-insights/src/utils/graphs.test.ts new file mode 100644 index 0000000000..47e7cae752 --- /dev/null +++ b/plugins/cost-insights/src/utils/graphs.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed 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 { formatGraphValue, tooltipItemOf } from './graphs'; +import { DataKey } from '../types'; + +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'); + }); + it('tooltipItemOf', () => { + expect( + tooltipItemOf('EUR', { + value: '1000', + color: 'red', + dataKey: DataKey.Current, + name: 'Kubernetes', + }), + ).toEqual({ + fill: 'red', + label: 'Kubernetes', + value: '€1,000', + }); + }); +}); diff --git a/plugins/cost-insights/src/utils/graphs.ts b/plugins/cost-insights/src/utils/graphs.ts index 2d63967c82..8f82d41054 100644 --- a/plugins/cost-insights/src/utils/graphs.ts +++ b/plugins/cost-insights/src/utils/graphs.ts @@ -45,8 +45,8 @@ export const tooltipItemOf = ( payload: Payload, ) => { const value = - typeof payload.value === 'number' - ? currencyFormatter(baseCurrency).format(payload.value) + payload.value && !isNaN(Number(payload.value)) + ? currencyFormatter(baseCurrency).format(Number(payload.value)) : payload.value; const fill = payload.color as string; From f373c294e2a938c8eee92262687ddaba947d13bf Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Tue, 15 Nov 2022 14:21:05 +0100 Subject: [PATCH 05/14] Updated the documentation of cost insight plugin Signed-off-by: bogdannechyporenko --- plugins/cost-insights/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index cc90b4d7a2..69910bdbe0 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -178,6 +178,17 @@ costInsights: name: Metric C ``` +### Base Currency (Optional) + +In the case you would like to show your baseline costs on the graph on other currency than US dollars. + +```yaml +## ./app-config.yaml +costInsights: + engineerCost: 200000 + baseCurrency: EUR +``` + ### Currencies (Optional) In the `Cost Overview` panel, users can choose from a dropdown of currencies to see costs in, such as Engineers or USD. Currencies must be defined as keys on the `currencies` field. A user-friendly label and unit are **required**. If not set, the `defaultCurrencies` in `currency.ts` will be used. From 21672221dab18caf7a29aa387b2f20f996d5d41f Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Tue, 15 Nov 2022 14:25:55 +0100 Subject: [PATCH 06/14] Fixed the typo Signed-off-by: bogdannechyporenko --- .changeset/four-adults-provide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/four-adults-provide.md b/.changeset/four-adults-provide.md index 9b9b9e1bef..f6f24bca75 100644 --- a/.changeset/four-adults-provide.md +++ b/.changeset/four-adults-provide.md @@ -2,4 +2,4 @@ '@backstage/plugin-cost-insights': minor --- -Provide the abilility to change the base currency from USD to any other currency in cost insights plugin +Provide the ability to change the base currency from USD to any other currency in cost insights plugin From e056128f8a57ece9b292f2f7216f6b080083e7f3 Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Tue, 15 Nov 2022 14:34:53 +0100 Subject: [PATCH 07/14] Prettified the code Signed-off-by: bogdannechyporenko --- plugins/cost-insights/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 69910bdbe0..eeafe9e029 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -180,7 +180,7 @@ costInsights: ### Base Currency (Optional) -In the case you would like to show your baseline costs on the graph on other currency than US dollars. +In the case you would like to show your baseline costs on the graph on other currency than US dollars. ```yaml ## ./app-config.yaml From 29648b9cddcf02c0e27c6206b438cc7cb4b6c884 Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Wed, 16 Nov 2022 21:03:47 +0100 Subject: [PATCH 08/14] Incorporated the feedback Signed-off-by: bogdannechyporenko --- app-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/app-config.yaml b/app-config.yaml index 7064d406f7..ef133491af 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -372,7 +372,6 @@ auth: myproxy: development: {} costInsights: - baseCurrency: 'EUR' engineerCost: 200000 products: computeEngine: From 5c1bfac3453c38ff0f47ae98fe7cfe94d77ad232 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Thu, 17 Nov 2022 15:12:26 +0100 Subject: [PATCH 09/14] 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, From ec3c41df0582709dab331d9beecfe8a004463dff Mon Sep 17 00:00:00 2001 From: bogdannechyporenko Date: Thu, 17 Nov 2022 16:23:24 +0100 Subject: [PATCH 10/14] Incorporated the feedback Signed-off-by: bogdannechyporenko --- app-config.yaml | 5 ++ plugins/cost-insights/README.md | 6 ++- plugins/cost-insights/config.d.ts | 49 ++++++++++++++++++- .../CostOverviewBreakdownChart.tsx | 3 +- .../CostOverviewCard/CostOverviewChart.tsx | 13 ++--- plugins/cost-insights/src/hooks/useConfig.tsx | 8 +-- .../cost-insights/src/testUtils/providers.tsx | 4 +- plugins/cost-insights/src/utils/currency.ts | 4 +- .../src/utils/formatters.test.ts | 6 +-- .../cost-insights/src/utils/graphs.test.ts | 16 +++--- 10 files changed, 84 insertions(+), 30 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index ef133491af..f890b4228a 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -373,6 +373,11 @@ auth: development: {} costInsights: engineerCost: 200000 + baseCurrency: + locale: en-US + options: + currency: EUR + minimumFractionDigits: 3 products: computeEngine: name: Compute Engine diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index eeafe9e029..945274202a 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -186,7 +186,11 @@ In the case you would like to show your baseline costs on the graph on other cur ## ./app-config.yaml costInsights: engineerCost: 200000 - baseCurrency: EUR + baseCurrency: + locale: en-US + options: + currency: EUR + minimumFractionDigits: 3 ``` ### Currencies (Optional) diff --git a/plugins/cost-insights/config.d.ts b/plugins/cost-insights/config.d.ts index 7c8b9d265d..5317485555 100644 --- a/plugins/cost-insights/config.d.ts +++ b/plugins/cost-insights/config.d.ts @@ -24,7 +24,54 @@ export interface Config { /** * @visibility frontend */ - baseCurrency?: Intl.NumberFormat; + baseCurrency?: { + /** + * @visibility frontend + */ + locale?: string; + options?: { + /** + * @visibility frontend + */ + localeMatcher?: string | undefined; + /** + * @visibility frontend + */ + style?: string | undefined; + /** + * @visibility frontend + */ + currency?: string | undefined; + /** + * @visibility frontend + */ + currencySign?: string | undefined; + /** + * @visibility frontend + */ + useGrouping?: boolean | undefined; + /** + * @visibility frontend + */ + minimumIntegerDigits?: number | undefined; + /** + * @visibility frontend + */ + minimumFractionDigits?: number | undefined; + /** + * @visibility frontend + */ + maximumFractionDigits?: number | undefined; + /** + * @visibility frontend + */ + minimumSignificantDigits?: number | undefined; + /** + * @visibility frontend + */ + maximumSignificantDigits?: number | undefined; + }; + }; products?: { [kind: string]: { diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx index 838da0d1f4..80a8d5f9ac 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx @@ -186,9 +186,10 @@ export const CostOverviewBreakdownChart = ({ ? DateTime.fromMillis(label) : DateTime.fromISO(label!); const dateTitle = date.toUTC().toFormat(DEFAULT_DATE_FORMAT); + const formatGraphValueWith = formatGraphValue(baseCurrency); const items = payload.map((p, i) => ({ label: p.dataKey as string, - value: formatGraphValue(baseCurrency)(Number(p.value), i), + value: formatGraphValueWith(Number(p.value), i), fill: p.color!, })); const expandText = ( diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx index 4232c56ffc..0a87474476 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -108,6 +108,7 @@ export const CostOverviewChart = ({ ? DateTime.fromMillis(label) : DateTime.fromISO(label!); const title = date.toUTC().toFormat(DEFAULT_DATE_FORMAT); + const formatGraphValueWith = formatGraphValue(baseCurrency); const items = payload .filter(p => dataKeys.includes(p.dataKey as string)) .map((p, i) => ({ @@ -117,16 +118,8 @@ export const CostOverviewChart = ({ : data.metric.name, value: p.dataKey === data.dailyCost.dataKey - ? formatGraphValue(baseCurrency)( - Number(p.value), - i, - data.dailyCost.format, - ) - : formatGraphValue(baseCurrency)( - Number(p.value), - i, - data.metric.format, - ), + ? formatGraphValueWith(Number(p.value), i, data.dailyCost.format) + : formatGraphValueWith(Number(p.value), i, data.metric.format), fill: p.dataKey === data.dailyCost.dataKey ? theme.palette.blue diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 34ecfd463d..679031b131 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 { createCurrency, defaultCurrencies } from '../utils/currency'; +import { createCurrencyFormat, defaultCurrencies } from '../utils/currency'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; /* @@ -80,7 +80,7 @@ export const ConfigContext = createContext( ); const defaultState: ConfigContextProps = { - baseCurrency: createCurrency(), + baseCurrency: createCurrencyFormat(), metrics: [], products: [], icons: [], @@ -124,11 +124,11 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { if (baseCurrency) { const options = baseCurrency.getOptionalConfig('options'); return new Intl.NumberFormat( - baseCurrency.getOptionalString('locales'), + baseCurrency.getOptionalString('locale'), options ? { localeMatcher: options.getOptionalString('localeMatcher'), - style: options.getOptionalString('style'), + style: 'currency', currency: options.getOptionalString('currency'), currencySign: options.getOptionalString('currencySign'), useGrouping: options.getOptionalBoolean('useGrouping'), diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 761a908ca4..bdba7c26fb 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -23,7 +23,7 @@ import { CurrencyContext, CurrencyContextProps } from '../hooks'; import { BillingDateContext, BillingDateContextProps } from '../hooks'; import { ScrollContext, ScrollContextProps } from '../hooks'; import { Duration } from '../types'; -import { createCurrency } from '../utils/currency'; +import { createCurrencyFormat } from '../utils/currency'; export type MockFilterProviderProps = PropsWithChildren< Partial @@ -81,7 +81,7 @@ export const MockConfigProvider = (props: MockConfigProviderProps) => { const { children, ...context } = props; const defaultContext: ConfigContextProps = { - baseCurrency: createCurrency(), + baseCurrency: createCurrencyFormat(), metrics: [], products: [], icons: [], diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts index 400d3bcb72..98c968fc53 100644 --- a/plugins/cost-insights/src/utils/currency.ts +++ b/plugins/cost-insights/src/utils/currency.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Currency, CurrencyType, Duration } from '../types'; -import { assertNever } from '../utils/assert'; +import { assertNever } from './assert'; export const rateOf = (cost: number, duration: Duration) => { switch (duration) { @@ -62,7 +62,7 @@ export const defaultCurrencies: Currency[] = [ }, ]; -export const createCurrency = ( +export const createCurrencyFormat = ( currency: string = 'USD', locale: string = 'en-US', ) => diff --git a/plugins/cost-insights/src/utils/formatters.test.ts b/plugins/cost-insights/src/utils/formatters.test.ts index f1cf778b6c..1190ac17ae 100644 --- a/plugins/cost-insights/src/utils/formatters.test.ts +++ b/plugins/cost-insights/src/utils/formatters.test.ts @@ -21,7 +21,7 @@ import { quarterOf, } from './formatters'; import { Duration } from '../types'; -import { createCurrency } from './currency'; +import { createCurrencyFormat } from './currency'; Date.now = jest.fn(() => new Date(Date.parse('2019-12-07')).valueOf()); @@ -40,7 +40,7 @@ describe('date formatters', () => { 0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566, ]; const formattedValues = values.map(val => - lengthyCurrencyFormatter(createCurrency()).format(val), + lengthyCurrencyFormatter(createCurrencyFormat()).format(val), ); expect(formattedValues).toEqual([ '$0.00000041', @@ -56,7 +56,7 @@ describe('date formatters', () => { 0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566, ]; const formattedValues = values.map(val => - lengthyCurrencyFormatter(createCurrency('EUR')).format(val), + lengthyCurrencyFormatter(createCurrencyFormat('EUR')).format(val), ); expect(formattedValues).toEqual([ '€0.00000041', diff --git a/plugins/cost-insights/src/utils/graphs.test.ts b/plugins/cost-insights/src/utils/graphs.test.ts index 5b425edb44..72749d7682 100644 --- a/plugins/cost-insights/src/utils/graphs.test.ts +++ b/plugins/cost-insights/src/utils/graphs.test.ts @@ -16,19 +16,23 @@ import { formatGraphValue, tooltipItemOf } from './graphs'; import { DataKey } from '../types'; -import { createCurrency } from './currency'; +import { createCurrencyFormat } from './currency'; describe('graphs', () => { it('formatGraphValue', () => { - expect(formatGraphValue(createCurrency('SEK'))(1000, 0)).toEqual( + expect(formatGraphValue(createCurrencyFormat('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'); + expect(formatGraphValue(createCurrencyFormat('EUR'))(1000, 0)).toEqual( + '€1,000', + ); + expect(formatGraphValue(createCurrencyFormat('USD'))(1000, 0)).toEqual( + '$1,000', + ); }); it('tooltipItemOf', () => { expect( - tooltipItemOf(createCurrency('EUR'))({ + tooltipItemOf(createCurrencyFormat('EUR'))({ value: '1000', color: 'red', dataKey: DataKey.Current, @@ -37,7 +41,7 @@ describe('graphs', () => { ).toEqual({ fill: 'red', label: 'Kubernetes', - value: '€1,000', + value: '€1,000.00', }); }); }); From 7018682a631967d22d529e990a208f0047317323 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Fri, 18 Nov 2022 16:30:38 +0100 Subject: [PATCH 11/14] Incorporated the feedback Signed-off-by: bnechyporenko --- app-config.yaml | 2 +- plugins/cost-insights/src/hooks/useConfig.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index f890b4228a..4a7e1cf9b8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -376,7 +376,7 @@ costInsights: baseCurrency: locale: en-US options: - currency: EUR + currency: USD minimumFractionDigits: 3 products: computeEngine: diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 679031b131..97bcdbd3b8 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -48,7 +48,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; * name: Metric B * baseCurrency: * locales: - * - en-US + * - nl-NL * options: * style: currency * currency: EUR From a7d5318c2107c021b0af56a3cf82d425ff79a347 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Fri, 18 Nov 2022 16:31:04 +0100 Subject: [PATCH 12/14] Incorporated the feedback Signed-off-by: bnechyporenko --- plugins/cost-insights/src/hooks/useConfig.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 97bcdbd3b8..680a682df7 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -47,8 +47,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; * metricB: * name: Metric B * baseCurrency: - * locales: - * - nl-NL + * locales: nl-NL * options: * style: currency * currency: EUR From 6cfdf88e058ae74c571cf884205763049e0c56ee Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Fri, 18 Nov 2022 16:31:22 +0100 Subject: [PATCH 13/14] Incorporated the feedback Signed-off-by: bnechyporenko --- plugins/cost-insights/src/hooks/useConfig.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 680a682df7..b1cfaf2002 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -47,7 +47,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; * metricB: * name: Metric B * baseCurrency: - * locales: nl-NL + * locale: nl-NL * options: * style: currency * currency: EUR From a149376b2e1c143de941e5c8df7f04e19ab83578 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Mon, 21 Nov 2022 18:23:01 +0100 Subject: [PATCH 14/14] Incorporated the feedback Signed-off-by: bnechyporenko --- app-config.yaml | 5 ----- plugins/cost-insights/README.md | 2 +- plugins/cost-insights/src/hooks/useConfig.tsx | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 4a7e1cf9b8..ef133491af 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -373,11 +373,6 @@ auth: development: {} costInsights: engineerCost: 200000 - baseCurrency: - locale: en-US - options: - currency: USD - minimumFractionDigits: 3 products: computeEngine: name: Compute Engine diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 945274202a..f65194551a 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -187,7 +187,7 @@ In the case you would like to show your baseline costs on the graph on other cur costInsights: engineerCost: 200000 baseCurrency: - locale: en-US + locale: nl-NL options: currency: EUR minimumFractionDigits: 3 diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index b1cfaf2002..6095f97d49 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -49,7 +49,6 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; * baseCurrency: * locale: nl-NL * options: - * style: currency * currency: EUR * minimumFractionDigits: 3 * currencies: