Merge pull request #14854 from bolcom/NS-648_configurable-engineer-threshold
feat: make engineer threshold configurable
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
added an optional config entry `costInsights.engineerThreshold` to allow users to control the threshold value for the 'negligible' change in costs.
|
||||
@@ -373,6 +373,7 @@ auth:
|
||||
development: {}
|
||||
costInsights:
|
||||
engineerCost: 200000
|
||||
engineerThreshold: 0.5
|
||||
products:
|
||||
computeEngine:
|
||||
name: Compute Engine
|
||||
|
||||
@@ -222,6 +222,19 @@ costInsights:
|
||||
rate: 3.5
|
||||
```
|
||||
|
||||
### Engineer Threshold (Optional; default 0.5)
|
||||
|
||||
This threshold determines whether to show 'Negligible', or a percentage with a fraction of 'engineers' for cost savings or cost excess on top of the charts.
|
||||
A threshold of 0.5 means that `Negligible` is shown when the difference in costs is lower than that fraction of engineers in that time frame,
|
||||
and show `XX% or ~N engineers` when it's above the threshold.
|
||||
|
||||
```yaml
|
||||
## ./app-config.yaml
|
||||
costInsights:
|
||||
engineerCost: 200000
|
||||
engineerThreshold: 0.5
|
||||
```
|
||||
|
||||
## Alerts
|
||||
|
||||
The CostInsightsApi `getAlerts` method may return any type of alert or recommendation (called collectively "Action Items" in Cost Insights) that implements the [Alert type](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/types/Alert.ts). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations.
|
||||
|
||||
@@ -237,6 +237,7 @@ export type ConfigContextProps = {
|
||||
products: Product[];
|
||||
icons: Icon[];
|
||||
engineerCost: number;
|
||||
engineerThreshold: number;
|
||||
currencies: Currency[];
|
||||
};
|
||||
|
||||
|
||||
Vendored
+5
@@ -21,6 +21,11 @@ export interface Config {
|
||||
*/
|
||||
engineerCost: number;
|
||||
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
engineerThreshold?: number;
|
||||
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { CostGrowth } from './CostGrowth';
|
||||
import { Currency, CurrencyType, Duration } from '../../types';
|
||||
import { ChangeThreshold, Currency, CurrencyType, Duration } from '../../types';
|
||||
import { findAlways } from '../../utils/assert';
|
||||
import { MockConfigProvider, MockCurrencyProvider } from '../../testUtils';
|
||||
import { defaultCurrencies } from '../../utils/currency';
|
||||
@@ -33,11 +33,16 @@ const MockContext = ({
|
||||
children,
|
||||
currency,
|
||||
engineerCost,
|
||||
engineerThreshold,
|
||||
}: PropsWithChildren<{
|
||||
currency: Currency;
|
||||
engineerCost: number;
|
||||
engineerThreshold?: number;
|
||||
}>) => (
|
||||
<MockConfigProvider engineerCost={engineerCost}>
|
||||
<MockConfigProvider
|
||||
engineerCost={engineerCost}
|
||||
engineerThreshold={engineerThreshold ?? 0.5}
|
||||
>
|
||||
<MockCurrencyProvider currency={currency}>{children}</MockCurrencyProvider>
|
||||
</MockConfigProvider>
|
||||
);
|
||||
@@ -104,3 +109,30 @@ describe.each`
|
||||
expect(getByText(expected)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
ratio | amount | threshold | expected
|
||||
${0} | ${0} | ${0} | ${'less than an engineer'}
|
||||
${0} | ${0} | ${200} | ${'Negligible'}
|
||||
${ChangeThreshold.lower} | ${0.5} | ${0} | ${'less than an engineer'}
|
||||
${ChangeThreshold.lower} | ${0.5} | ${0.5} | ${'Negligible'}
|
||||
${ChangeThreshold.upper} | ${0.5} | ${0.000001} | ${'less than an engineer'}
|
||||
${ChangeThreshold.upper} | ${0.5} | ${0.5} | ${'Negligible'}
|
||||
${3} | ${500_000} | ${0} | ${`300% or ~30 engineers`}
|
||||
${3} | ${500_000} | ${0.5} | ${`300% or ~30 engineers`}
|
||||
${3} | ${500_000} | ${29} | ${'300% or ~30 engineers'}
|
||||
${3} | ${500_000} | ${30} | ${'Negligible'}
|
||||
`('<CostGrowth />', ({ ratio, amount, threshold, expected }) => {
|
||||
it(`should display the correct difference the threshold is different. ratio: ${ratio} threshold:${threshold} expected:${expected}`, async () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<MockContext
|
||||
engineerCost={200_000}
|
||||
engineerThreshold={threshold}
|
||||
currency={engineers}
|
||||
>
|
||||
<CostGrowth change={{ ratio, amount }} duration={Duration.P30D} />
|
||||
</MockContext>,
|
||||
);
|
||||
expect(getByText(expected)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,12 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
CurrencyType,
|
||||
Duration,
|
||||
EngineerThreshold,
|
||||
GrowthType,
|
||||
} from '../../types';
|
||||
import { CurrencyType, Duration, GrowthType } from '../../types';
|
||||
import { ChangeStatistic } from '@backstage/plugin-cost-insights-common';
|
||||
import { rateOf } from '../../utils/currency';
|
||||
import { growthOf } from '../../utils/change';
|
||||
@@ -42,7 +37,7 @@ export const CostGrowth = (props: CostGrowthProps) => {
|
||||
const { change, duration } = props;
|
||||
|
||||
const styles = useStyles();
|
||||
const { engineerCost } = useConfig();
|
||||
const { engineerCost, engineerThreshold } = useConfig();
|
||||
const [currency] = useCurrency();
|
||||
|
||||
// Only display costs in absolute values
|
||||
@@ -55,7 +50,7 @@ export const CostGrowth = (props: CostGrowthProps) => {
|
||||
|
||||
// If a ratio cannot be calculated, don't format.
|
||||
const growth = notEmpty(change.ratio)
|
||||
? growthOf({ ratio: change.ratio, amount: engineers })
|
||||
? growthOf({ ratio: change.ratio, amount: engineers }, engineerThreshold)
|
||||
: null;
|
||||
// Determine if growth is significant enough to highlight
|
||||
const classes = classnames({
|
||||
@@ -63,7 +58,7 @@ export const CostGrowth = (props: CostGrowthProps) => {
|
||||
[styles.savings]: growth === GrowthType.Savings,
|
||||
});
|
||||
|
||||
if (engineers < EngineerThreshold) {
|
||||
if (engineers < engineerThreshold) {
|
||||
return <span className={classes}>Negligible</span>;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { CostGrowthIndicator } from './CostGrowthIndicator';
|
||||
import { ChangeThreshold, EngineerThreshold } from '../../types';
|
||||
import { MockConfigProvider } from '../../testUtils';
|
||||
|
||||
describe.each`
|
||||
ratio | amount | ariaLabel
|
||||
@@ -30,7 +31,9 @@ describe.each`
|
||||
`('growthOf', ({ ratio, amount, ariaLabel }) => {
|
||||
it(`should display the correct indicator for ${ariaLabel}`, async () => {
|
||||
const { getByLabelText } = await renderInTestApp(
|
||||
<CostGrowthIndicator change={{ ratio, amount }} />,
|
||||
<MockConfigProvider engineerThreshold={EngineerThreshold}>
|
||||
<CostGrowthIndicator change={{ ratio, amount }} />
|
||||
</MockConfigProvider>,
|
||||
);
|
||||
expect(getByLabelText(ariaLabel)).toBeInTheDocument();
|
||||
});
|
||||
@@ -48,7 +51,9 @@ describe.each`
|
||||
`('growthOf', ({ ratio, amount }) => {
|
||||
it('should display the correct indicator for negligible growth', async () => {
|
||||
const { queryByLabelText } = await renderInTestApp(
|
||||
<CostGrowthIndicator change={{ ratio, amount }} />,
|
||||
<MockConfigProvider engineerThreshold={EngineerThreshold}>
|
||||
<CostGrowthIndicator change={{ ratio, amount }} />
|
||||
</MockConfigProvider>,
|
||||
);
|
||||
expect(queryByLabelText('savings')).not.toBeInTheDocument();
|
||||
expect(queryByLabelText('excess')).not.toBeInTheDocument();
|
||||
|
||||
@@ -23,6 +23,7 @@ import { growthOf } from '../../utils/change';
|
||||
import { GrowthType } from '../../types';
|
||||
import { useCostGrowthStyles as useStyles } from '../../utils/styles';
|
||||
import { ChangeStatistic, Maybe } from '@backstage/plugin-cost-insights-common';
|
||||
import { useConfig } from '../../hooks';
|
||||
|
||||
/** @public */
|
||||
export type CostGrowthIndicatorProps = TypographyProps & {
|
||||
@@ -35,10 +36,12 @@ export type CostGrowthIndicatorProps = TypographyProps & {
|
||||
|
||||
/** @public */
|
||||
export const CostGrowthIndicator = (props: CostGrowthIndicatorProps) => {
|
||||
const { engineerThreshold } = useConfig();
|
||||
const { change, formatter, className, ...extraProps } = props;
|
||||
|
||||
const classes = useStyles();
|
||||
const growth = growthOf(change);
|
||||
|
||||
const growth = growthOf(change, engineerThreshold);
|
||||
|
||||
const classNames = classnames(classes.indicator, className, {
|
||||
[classes.excess]: growth === GrowthType.Excess,
|
||||
|
||||
@@ -22,7 +22,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Config as BackstageConfig } from '@backstage/config';
|
||||
import { Currency, Icon, Metric, Product } from '../types';
|
||||
import { Currency, EngineerThreshold, Icon, Metric, Product } from '../types';
|
||||
import { getIcon } from '../utils/navigation';
|
||||
import { validateCurrencies, validateMetrics } from '../utils/config';
|
||||
import { createCurrencyFormat, defaultCurrencies } from '../utils/currency';
|
||||
@@ -70,6 +70,7 @@ export type ConfigContextProps = {
|
||||
products: Product[];
|
||||
icons: Icon[];
|
||||
engineerCost: number;
|
||||
engineerThreshold: number;
|
||||
currencies: Currency[];
|
||||
};
|
||||
|
||||
@@ -83,6 +84,7 @@ const defaultState: ConfigContextProps = {
|
||||
products: [],
|
||||
icons: [],
|
||||
engineerCost: 0,
|
||||
engineerThreshold: EngineerThreshold,
|
||||
currencies: defaultCurrencies,
|
||||
};
|
||||
|
||||
@@ -183,11 +185,19 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
|
||||
return c.getNumber('costInsights.engineerCost');
|
||||
}
|
||||
|
||||
function getEngineerThreshold(): number {
|
||||
return (
|
||||
c.getOptionalNumber('costInsights.engineerThreshold') ??
|
||||
defaultState.engineerThreshold
|
||||
);
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
const baseCurrency = getBaseCurrency();
|
||||
const products = getProducts();
|
||||
const metrics = getMetrics();
|
||||
const engineerCost = getEngineerCost();
|
||||
const engineerThreshold = getEngineerThreshold();
|
||||
const icons = getIcons();
|
||||
const currencies = getCurrencies();
|
||||
|
||||
@@ -200,6 +210,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
|
||||
metrics,
|
||||
products,
|
||||
engineerCost,
|
||||
engineerThreshold,
|
||||
icons,
|
||||
currencies,
|
||||
}));
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
ScrollContext,
|
||||
ScrollContextProps,
|
||||
} from '../hooks';
|
||||
import { Duration } from '../types';
|
||||
import { Duration, EngineerThreshold } from '../types';
|
||||
import { createCurrencyFormat } from '../utils/currency';
|
||||
|
||||
export type MockFilterProviderProps = PropsWithChildren<
|
||||
@@ -95,6 +95,7 @@ export const MockConfigProvider = (props: MockConfigProviderProps) => {
|
||||
products: [],
|
||||
icons: [],
|
||||
engineerCost: 0,
|
||||
engineerThreshold: EngineerThreshold,
|
||||
currencies: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ describe.each`
|
||||
expected: GrowthType;
|
||||
}) => {
|
||||
it(`should display ${GrowthMap[expected]}`, () => {
|
||||
expect(growthOf({ ratio, amount })).toBe(expected);
|
||||
expect(growthOf({ ratio, amount }, EngineerThreshold)).toBe(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Cost,
|
||||
ChangeStatistic,
|
||||
ChangeThreshold,
|
||||
EngineerThreshold,
|
||||
GrowthType,
|
||||
MetricData,
|
||||
Duration,
|
||||
@@ -29,8 +28,11 @@ import { inclusiveStartDateOf } from './duration';
|
||||
import { notEmpty } from './assert';
|
||||
|
||||
// Used for displaying status colors
|
||||
export function growthOf(change: ChangeStatistic): GrowthType {
|
||||
const exceedsEngineerThreshold = Math.abs(change.amount) >= EngineerThreshold;
|
||||
export function growthOf(
|
||||
change: ChangeStatistic,
|
||||
engineerThreshold: number,
|
||||
): GrowthType {
|
||||
const exceedsEngineerThreshold = Math.abs(change.amount) >= engineerThreshold;
|
||||
|
||||
if (notEmpty(change.ratio)) {
|
||||
if (exceedsEngineerThreshold && change.ratio >= ChangeThreshold.upper) {
|
||||
@@ -41,9 +43,12 @@ export function growthOf(change: ChangeStatistic): GrowthType {
|
||||
return GrowthType.Savings;
|
||||
}
|
||||
} else {
|
||||
if (exceedsEngineerThreshold && change.amount > 0) return GrowthType.Excess;
|
||||
if (exceedsEngineerThreshold && change.amount < 0)
|
||||
if (exceedsEngineerThreshold && change.amount > 0) {
|
||||
return GrowthType.Excess;
|
||||
}
|
||||
if (exceedsEngineerThreshold && change.amount < 0) {
|
||||
return GrowthType.Savings;
|
||||
}
|
||||
}
|
||||
|
||||
return GrowthType.Negligible;
|
||||
|
||||
Reference in New Issue
Block a user