Merge pull request #14854 from bolcom/NS-648_configurable-engineer-threshold

feat: make engineer threshold configurable
This commit is contained in:
Fredrik Adelöw
2022-11-25 20:44:32 +01:00
committed by GitHub
13 changed files with 99 additions and 22 deletions
+13
View File
@@ -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.
+1
View File
@@ -237,6 +237,7 @@ export type ConfigContextProps = {
products: Product[];
icons: Icon[];
engineerCost: number;
engineerThreshold: number;
currencies: Currency[];
};
+5
View File
@@ -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,
+12 -1
View File
@@ -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);
});
},
);
+10 -5
View File
@@ -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;