diff --git a/.changeset/four-adults-provide.md b/.changeset/four-adults-provide.md
new file mode 100644
index 0000000000..0edb4807d9
--- /dev/null
+++ b/.changeset/four-adults-provide.md
@@ -0,0 +1,5 @@
+---
+'@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/README.md b/plugins/cost-insights/README.md
index cc90b4d7a2..f65194551a 100644
--- a/plugins/cost-insights/README.md
+++ b/plugins/cost-insights/README.md
@@ -178,6 +178,21 @@ 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:
+ locale: nl-NL
+ options:
+ currency: EUR
+ minimumFractionDigits: 3
+```
+
### 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.
diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md
index dd4e404450..b4c818f1e4 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: 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 4144b6d6a5..5317485555 100644
--- a/plugins/cost-insights/config.d.ts
+++ b/plugins/cost-insights/config.d.ts
@@ -21,6 +21,58 @@ export interface Config {
*/
engineerCost: number;
+ /**
+ * @visibility frontend
+ */
+ 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/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..fa4ae18e24 100644
--- a/plugins/cost-insights/src/components/BarChart/BarChart.tsx
+++ b/plugins/cost-insights/src/components/BarChart/BarChart.tsx
@@ -40,20 +40,24 @@ 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: Intl.NumberFormat) => {
+ 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(tooltipItemOf(baseCurrency)).filter(notEmpty);
+ return (
+
+ {items.map((item, index) => (
+
+ ))}
+
+ );
+ };
+ return tooltip;
};
/** @public */
@@ -69,12 +73,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 +170,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..80a8d5f9ac 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);
@@ -185,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(Number(p.value), i),
+ value: formatGraphValueWith(Number(p.value), i),
fill: p.color!,
}));
const expandText = (
@@ -253,7 +255,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..0a87474476 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: {
@@ -106,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) => ({
@@ -115,8 +118,8 @@ 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),
+ ? 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
@@ -157,7 +160,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..c20c7d4a1f 100644
--- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx
+++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx
@@ -17,11 +17,12 @@
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';
import { Table, TableColumn } from '@backstage/core-components';
+import { useConfig } from '../../hooks';
export type ProductEntityTableOptions = Partial<
Pick
@@ -35,36 +36,38 @@ type RowData = {
change: ChangeStatistic;
};
-function createRenderer(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.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.
@@ -99,6 +102,7 @@ export const ProductEntityTable = ({
options,
}: ProductEntityTableProps) => {
const classes = useStyles();
+ const { baseCurrency } = useConfig();
const entities = entity.entities[entityLabel];
const data = Object.assign(
@@ -116,7 +120,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 +130,7 @@ export const ProductEntityTable = ({
{data.previousName}
),
align: 'right',
- render: createRenderer('previous', classes),
+ render: createRenderer(baseCurrency)('previous', classes),
customSort: createSorter('previous'),
},
{
@@ -135,14 +139,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..f44e24f443 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,7 @@ export const ProductInsightsChart = ({
const id = label === '' ? null : label;
const title = titleOf(label);
- const items = payload.map(tooltipItemOf).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/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..6095f97d49 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 { createCurrencyFormat, defaultCurrencies } from '../utils/currency';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
/*
@@ -46,6 +46,11 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api';
* default: true
* metricB:
* name: Metric B
+ * baseCurrency:
+ * locale: nl-NL
+ * options:
+ * currency: EUR
+ * minimumFractionDigits: 3
* currencies:
* currencyA:
* label: Currency A
@@ -60,6 +65,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api';
/** @public */
export type ConfigContextProps = {
+ baseCurrency: Intl.NumberFormat;
metrics: Metric[];
products: Product[];
icons: Icon[];
@@ -72,6 +78,7 @@ export const ConfigContext = createContext(
);
const defaultState: ConfigContextProps = {
+ baseCurrency: createCurrencyFormat(),
metrics: [],
products: [],
icons: [],
@@ -110,6 +117,42 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
return [];
}
+ function getBaseCurrency(): Intl.NumberFormat {
+ const baseCurrency = c.getOptionalConfig('costInsights.baseCurrency');
+ if (baseCurrency) {
+ const options = baseCurrency.getOptionalConfig('options');
+ return new Intl.NumberFormat(
+ baseCurrency.getOptionalString('locale'),
+ options
+ ? {
+ localeMatcher: options.getOptionalString('localeMatcher'),
+ style: 'currency',
+ 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;
+ }
+
function getCurrencies(): Currency[] {
const currencies = c.getOptionalConfig('costInsights.currencies');
if (currencies) {
@@ -141,6 +184,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
}
function getConfig() {
+ const baseCurrency = getBaseCurrency();
const products = getProducts();
const metrics = getMetrics();
const engineerCost = getEngineerCost();
@@ -152,6 +196,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..bdba7c26fb 100644
--- a/plugins/cost-insights/src/testUtils/providers.tsx
+++ b/plugins/cost-insights/src/testUtils/providers.tsx
@@ -15,19 +15,15 @@
*/
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 { Group, Duration } from '../types';
-
-export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }];
+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 { Duration } from '../types';
+import { createCurrencyFormat } from '../utils/currency';
export type MockFilterProviderProps = PropsWithChildren<
Partial
@@ -85,6 +81,7 @@ export const MockConfigProvider = (props: MockConfigProviderProps) => {
const { children, ...context } = props;
const defaultContext: ConfigContextProps = {
+ baseCurrency: createCurrencyFormat(),
metrics: [],
products: [],
icons: [],
diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts
index 60115aad3a..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) {
@@ -61,3 +61,12 @@ export const defaultCurrencies: Currency[] = [
rate: 5.5,
},
];
+
+export const createCurrencyFormat = (
+ 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 8bf0a6a9d7..1190ac17ae 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 { createCurrencyFormat } 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.format(val),
+ lengthyCurrencyFormatter(createCurrencyFormat()).format(val),
);
expect(formattedValues).toEqual([
'$0.00000041',
@@ -49,6 +50,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(createCurrencyFormat('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/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts
index e9b19097a4..fd3fb4c0ef 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 currencyFormatter = (currency: Intl.NumberFormat) => {
+ const options = currency.resolvedOptions();
-export const currencyFormatter = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
-});
+ return new Intl.NumberFormat(options.locale, {
+ style: 'currency',
+ currency: options.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: Intl.NumberFormat) => {
+ const options = currency.resolvedOptions();
+
+ return new Intl.NumberFormat(options.locale, {
+ style: '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
new file mode 100644
index 0000000000..72749d7682
--- /dev/null
+++ b/plugins/cost-insights/src/utils/graphs.test.ts
@@ -0,0 +1,47 @@
+/*
+ * 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';
+import { createCurrencyFormat } from './currency';
+
+describe('graphs', () => {
+ it('formatGraphValue', () => {
+ expect(formatGraphValue(createCurrencyFormat('SEK'))(1000, 0)).toEqual(
+ 'SEK 1,000',
+ );
+ expect(formatGraphValue(createCurrencyFormat('EUR'))(1000, 0)).toEqual(
+ '€1,000',
+ );
+ expect(formatGraphValue(createCurrencyFormat('USD'))(1000, 0)).toEqual(
+ '$1,000',
+ );
+ });
+ it('tooltipItemOf', () => {
+ expect(
+ tooltipItemOf(createCurrencyFormat('EUR'))({
+ value: '1000',
+ color: 'red',
+ dataKey: DataKey.Current,
+ name: 'Kubernetes',
+ }),
+ ).toEqual({
+ fill: 'red',
+ label: 'Kubernetes',
+ value: '€1,000.00',
+ });
+ });
+});
diff --git a/plugins/cost-insights/src/utils/graphs.ts b/plugins/cost-insights/src/utils/graphs.ts
index 172f6cd7e3..896d09f865 100644
--- a/plugins/cost-insights/src/utils/graphs.ts
+++ b/plugins/cost-insights/src/utils/graphs.ts
@@ -23,44 +23,43 @@ import {
lengthyCurrencyFormatter,
} from './formatters';
-export function formatGraphValue(
- value: number,
- _index: number,
- format?: string,
-) {
- if (format === 'number') {
- return value.toLocaleString();
- }
+export const formatGraphValue =
+ (baseCurrency: Intl.NumberFormat) =>
+ (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) => {
- const value =
- typeof payload.value === 'number'
- ? currencyFormatter.format(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,