Cost Insights Configurable Default Currency
Signed-off-by: bnechyporenko <bnechyporenko@bol.com>
This commit is contained in:
@@ -372,6 +372,7 @@ auth:
|
||||
myproxy:
|
||||
development: {}
|
||||
costInsights:
|
||||
baseCurrency: 'EUR'
|
||||
engineerCost: 200000
|
||||
products:
|
||||
computeEngine:
|
||||
|
||||
Vendored
+5
@@ -21,6 +21,11 @@ export interface Config {
|
||||
*/
|
||||
engineerCost: number;
|
||||
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseCurrency?: string;
|
||||
|
||||
products?: {
|
||||
[kind: string]: {
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
<BarChart
|
||||
responsive={responsive}
|
||||
displayAmount={displayAmount}
|
||||
resources={resources}
|
||||
/>,
|
||||
<MockConfigProvider>
|
||||
<BarChart
|
||||
responsive={responsive}
|
||||
displayAmount={displayAmount}
|
||||
resources={resources}
|
||||
/>
|
||||
</MockConfigProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<BarChartTooltip title={title}>
|
||||
{items.map((item, index) => (
|
||||
<BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
|
||||
))}
|
||||
</BarChartTooltip>
|
||||
);
|
||||
const title = titleOf(label);
|
||||
const items = payload
|
||||
.map(p => tooltipItemOf(baseCurrency, p))
|
||||
.filter(notEmpty);
|
||||
return (
|
||||
<BarChartTooltip title={title}>
|
||||
{items.map((item, index) => (
|
||||
<BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
|
||||
))}
|
||||
</BarChartTooltip>
|
||||
);
|
||||
};
|
||||
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}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={currencyFormatter.format}
|
||||
tickFormatter={currencyFormatter(baseCurrency).format}
|
||||
domain={[() => 0, globalResourcesMax]}
|
||||
tick={styles.axis}
|
||||
/>
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { BarChartLegend } from './BarChartLegend';
|
||||
import { MockConfigProvider } from '../../testUtils';
|
||||
|
||||
describe('<BarChartLegend />', () => {
|
||||
it(`Should display the correct cost start and end`, async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<BarChartLegend costStart={1000} costEnd={5000} />,
|
||||
<MockConfigProvider>
|
||||
<BarChartLegend costStart={1000} costEnd={5000} />,
|
||||
</MockConfigProvider>,
|
||||
);
|
||||
expect(rendered.getByText(/\$1,000/)).toBeInTheDocument();
|
||||
expect(rendered.queryByText(/\$5,000/)).toBeInTheDocument();
|
||||
|
||||
@@ -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<CostInsightsTheme>();
|
||||
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)}
|
||||
</LegendItem>
|
||||
</Box>
|
||||
<Box marginRight={2}>
|
||||
@@ -71,7 +73,7 @@ export const BarChartLegend = (
|
||||
title={data.currentName}
|
||||
markerColor={options.hideMarker ? undefined : data.currentFill}
|
||||
>
|
||||
{currencyFormatter.format(costEnd)}
|
||||
{currencyFormatter(baseCurrency).format(costEnd)}
|
||||
</LegendItem>
|
||||
</Box>
|
||||
{children}
|
||||
|
||||
+15
-14
@@ -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<CostInsightsTheme>();
|
||||
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 = ({
|
||||
<YAxis
|
||||
domain={[() => 0, 'dataMax']}
|
||||
tick={{ fill: classes.axis.fill }}
|
||||
tickFormatter={formatGraphValue}
|
||||
tickFormatter={formatGraphValue(baseCurrency)}
|
||||
width={classes.yAxis.width}
|
||||
/>
|
||||
{renderAreas()}
|
||||
|
||||
@@ -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<Metric>;
|
||||
@@ -65,6 +66,7 @@ export const CostOverviewChart = ({
|
||||
}: CostOverviewChartProps) => {
|
||||
const theme = useTheme<CostInsightsTheme>();
|
||||
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 = ({
|
||||
<YAxis
|
||||
domain={[() => 0, 'dataMax']}
|
||||
tick={{ fill: styles.axis.fill }}
|
||||
tickFormatter={formatGraphValue}
|
||||
tickFormatter={formatGraphValue(baseCurrency)}
|
||||
width={styles.yAxis.width}
|
||||
yAxisId={data.dailyCost.dataKey}
|
||||
/>
|
||||
|
||||
+15
-10
@@ -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('<ProductEntityDialog/>', () => {
|
||||
it('Should show a tab for a single sub-entity type', () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<ProductEntityDialog
|
||||
open
|
||||
entity={singleBreakdownEntity}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
<MockConfigProvider>
|
||||
<ProductEntityDialog
|
||||
open
|
||||
entity={singleBreakdownEntity}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
</MockConfigProvider>,
|
||||
),
|
||||
);
|
||||
expect(getByText('Breakdown by SKU')).toBeInTheDocument();
|
||||
@@ -99,11 +102,13 @@ describe('<ProductEntityDialog/>', () => {
|
||||
it('Should show tabs when multiple sub-entity types exist', () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<ProductEntityDialog
|
||||
open
|
||||
entity={multiBreakdownEntity}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
<MockConfigProvider>
|
||||
<ProductEntityDialog
|
||||
open
|
||||
entity={multiBreakdownEntity}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
</MockConfigProvider>,
|
||||
),
|
||||
);
|
||||
expect(getByText('Breakdown by SKU')).toBeInTheDocument();
|
||||
|
||||
@@ -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<BarChartOptions, 'previousName' | 'currentName'>
|
||||
@@ -35,7 +36,11 @@ type RowData = {
|
||||
change: ChangeStatistic;
|
||||
};
|
||||
|
||||
function createRenderer(col: keyof RowData, classes: Record<string, string>) {
|
||||
function createRenderer(
|
||||
baseCurrency: string,
|
||||
col: keyof RowData,
|
||||
classes: Record<string, string>,
|
||||
) {
|
||||
return function render(rowData: {}): JSX.Element {
|
||||
const row = rowData as RowData;
|
||||
const rowStyles = classnames(classes.row, {
|
||||
@@ -49,7 +54,7 @@ function createRenderer(col: keyof RowData, classes: Record<string, string>) {
|
||||
case 'current':
|
||||
return (
|
||||
<Typography className={rowStyles}>
|
||||
{costFormatter.format(row[col])}
|
||||
{costFormatter(baseCurrency).format(row[col])}
|
||||
</Typography>
|
||||
);
|
||||
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: <Typography className={firstColClasses}>{entityLabel}</Typography>,
|
||||
render: createRenderer('label', classes),
|
||||
render: createRenderer(baseCurrency, 'label', classes),
|
||||
customSort: createSorter('label'),
|
||||
width: '33.33%',
|
||||
},
|
||||
@@ -126,7 +132,7 @@ export const ProductEntityTable = ({
|
||||
<Typography className={classes.column}>{data.previousName}</Typography>
|
||||
),
|
||||
align: 'right',
|
||||
render: createRenderer('previous', classes),
|
||||
render: createRenderer(baseCurrency, 'previous', classes),
|
||||
customSort: createSorter('previous'),
|
||||
},
|
||||
{
|
||||
@@ -135,14 +141,14 @@ export const ProductEntityTable = ({
|
||||
<Typography className={classes.column}>{data.currentName}</Typography>
|
||||
),
|
||||
align: 'right',
|
||||
render: createRenderer('current', classes),
|
||||
render: createRenderer(baseCurrency, 'current', classes),
|
||||
customSort: createSorter('current'),
|
||||
},
|
||||
{
|
||||
field: 'change',
|
||||
title: <Typography className={lastColClasses}>Change</Typography>,
|
||||
align: 'right',
|
||||
render: createRenderer('change', classes),
|
||||
render: createRenderer(baseCurrency, 'change', classes),
|
||||
customSort: createSorter('change'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
|
||||
+11
-6
@@ -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('<UnlabeledDataflowAlertCard />', () => {
|
||||
'projects with unlabeled Dataflow jobs in the last 30 days.',
|
||||
);
|
||||
const rendered = await renderInTestApp(
|
||||
<UnlabeledDataflowAlertCard
|
||||
alert={MockUnlabeledDataflowAlertMultipleProjects}
|
||||
/>,
|
||||
<MockConfigProvider>
|
||||
<UnlabeledDataflowAlertCard
|
||||
alert={MockUnlabeledDataflowAlertMultipleProjects}
|
||||
/>
|
||||
</MockConfigProvider>,
|
||||
);
|
||||
expect(rendered.getByText(subheader)).toBeInTheDocument();
|
||||
});
|
||||
@@ -71,9 +74,11 @@ describe('<UnlabeledDataflowAlertCard />', () => {
|
||||
it('renders the correct subheader for a single project', async () => {
|
||||
const subheader = new RegExp('1 project');
|
||||
const rendered = await renderInTestApp(
|
||||
<UnlabeledDataflowAlertCard
|
||||
alert={MockUnlabeledDataflowAlertSingleProject}
|
||||
/>,
|
||||
<MockConfigProvider>
|
||||
<UnlabeledDataflowAlertCard
|
||||
alert={MockUnlabeledDataflowAlertSingleProject}
|
||||
/>
|
||||
</MockConfigProvider>,
|
||||
);
|
||||
expect(rendered.getByText(subheader)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -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<ConfigContextProps | undefined>(
|
||||
);
|
||||
|
||||
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,
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string>) => {
|
||||
export const tooltipItemOf = (
|
||||
baseCurrency: string,
|
||||
payload: Payload<string, string>,
|
||||
) => {
|
||||
const value =
|
||||
typeof payload.value === 'number'
|
||||
? currencyFormatter.format(payload.value)
|
||||
? currencyFormatter(baseCurrency).format(payload.value)
|
||||
: payload.value;
|
||||
const fill = payload.color as string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user