diff --git a/.changeset/large-files-beg.md b/.changeset/large-files-beg.md new file mode 100644 index 0000000000..f888fc7173 --- /dev/null +++ b/.changeset/large-files-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Added optional currencies config to allow overriding the default currencies. diff --git a/app-config.yaml b/app-config.yaml index d2c8bf8074..1c0ae98c3e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -384,6 +384,31 @@ costInsights: default: true MSC: name: Monthly Subscribers + currencies: + engineers: + label: 'Engineers 🛠' + unit: 'engineer' + usd: + label: 'US Dollars 💵' + kind: 'USD' + unit: 'dollar' + prefix: '$' + rate: 1 + carbonOffsetTons: + label: 'Carbon Offset Tons ♻️⚖️s' + kind: 'CARBON_OFFSET_TONS' + unit: 'carbon offset ton' + rate: 3.5 + beers: + label: 'Beers 🍺' + kind: 'BEERS' + unit: 'beer' + rate: 4.5 + pintsIceCream: + label: 'Pints of Ice Cream 🍦' + kind: 'PINTS_OF_ICE_CREAM' + unit: 'ice cream pint' + rate: 5.5 homepage: clocks: - label: UTC diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index cc6d47c69b..187ab78b73 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -148,6 +148,36 @@ costInsights: name: Metric C ``` +### 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 `currenc.ts` will be used. + +A currency without `kind` is reserved to calculate cost for `engineers`. There should only be one currency without `kind`. + +```yaml +## ./app-config.yaml +costInsights: + engineerCost: 200000 + products: + productA: + name: Some Cloud Product + icon: storage + productB: + name: Some Other Cloud Product + icon: data + currencies: + metricA: + currencyA: + label: Currency A + unit: Unit A + currencyB: + label: Currency B + kind: CURRENCY_B + unit: Unit B + prefix: B + rate: 3.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. diff --git a/plugins/cost-insights/config.d.ts b/plugins/cost-insights/config.d.ts index 9b33659db6..3d4e649b67 100644 --- a/plugins/cost-insights/config.d.ts +++ b/plugins/cost-insights/config.d.ts @@ -48,5 +48,39 @@ interface Config { default?: boolean; }; }; + + currencies?: { + [id: string]: { + /** + * @visibility frontend + */ + label: string; + + /** + * @visibility frontend + */ + unit: string; + + /** + * @visibility frontend + */ + kind?: string; + + /** + * @visibility frontend + */ + prefix?: string; + + /** + * @visibility frontend + */ + rate?: number; + + /** + * @visibility frontend + */ + default?: boolean; + }; + }; }; } diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index a2a33b0c92..451a48b71f 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -24,12 +24,12 @@ import React, { import { Config as BackstageConfig } from '@backstage/config'; import { Currency, Icon, Metric, Product } from '../types'; import { getIcon } from '../utils/navigation'; -import { validateMetrics } from '../utils/config'; +import { validateCurrencies, validateMetrics } from '../utils/config'; import { defaultCurrencies } from '../utils/currency'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; /* - * Config schema 2020-10-15 + * Config schema 2021-08-05 * * costInsights: * engineerCost: 200000 @@ -46,6 +46,16 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; * default: true * metricB: * name: Metric B + * currencies: + * currencyA: + * label: Currency A + * unit: Unit A + * currencyB: + * label: Currency B + * kind: CURRENCY_B + * unit: Unit B + * prefix: B + * rate: 3.5 */ export type ConfigContextProps = { @@ -96,6 +106,21 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { return []; } + function getCurrencies(): Currency[] { + const currencies = c.getOptionalConfig('costInsights.currencies'); + if (currencies) { + return currencies.keys().map(key => ({ + label: currencies.getString(`${key}.label`), + unit: currencies.getString(`${key}.unit`), + kind: currencies.getOptionalString(`${key}.kind`) || null, + prefix: currencies.getOptionalString(`${key}.prefix`), + rate: currencies.getOptionalNumber(`${key}.rate`), + })); + } + + return defaultCurrencies; + } + function getIcons(): Icon[] { const products = c.getConfig('costInsights.products'); const keys = products.keys(); @@ -115,8 +140,10 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { const metrics = getMetrics(); const engineerCost = getEngineerCost(); const icons = getIcons(); + const currencies = getCurrencies(); validateMetrics(metrics); + validateCurrencies(currencies); setConfig(prevState => ({ ...prevState, @@ -124,6 +151,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { products, engineerCost, icons, + currencies, })); setLoading(false); diff --git a/plugins/cost-insights/src/hooks/useCurrency.tsx b/plugins/cost-insights/src/hooks/useCurrency.tsx index 832ca8deea..13458ee432 100644 --- a/plugins/cost-insights/src/hooks/useCurrency.tsx +++ b/plugins/cost-insights/src/hooks/useCurrency.tsx @@ -21,8 +21,7 @@ import React, { PropsWithChildren, } from 'react'; import { Currency } from '../types'; -import { findAlways } from '../utils/assert'; -import { defaultCurrencies } from '../utils/currency'; +import { useConfig } from './useConfig'; export type CurrencyContextProps = { currency: Currency; @@ -34,8 +33,12 @@ export const CurrencyContext = React.createContext< >(undefined); export const CurrencyProvider = ({ children }: PropsWithChildren<{}>) => { - const engineers = findAlways(defaultCurrencies, c => c.kind === null); - const [currency, setCurrency] = useState(engineers); + const config = useConfig(); + const currencies = config.currencies; + const engineers = currencies.find(currency => currency.kind === null); + const [currency, setCurrency] = useState( + engineers || currencies[0], + ); return ( {children} diff --git a/plugins/cost-insights/src/utils/config.ts b/plugins/cost-insights/src/utils/config.ts index 078bd7f066..496021d70e 100644 --- a/plugins/cost-insights/src/utils/config.ts +++ b/plugins/cost-insights/src/utils/config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Metric } from '../types'; +import { Metric, Currency } from '../types'; export function validateMetrics(metrics: Metric[]) { const defaults = metrics.filter(metric => metric.default); @@ -24,3 +24,12 @@ export function validateMetrics(metrics: Metric[]) { ); } } + +export function validateCurrencies(currencies: Currency[]) { + const withoutKinds = currencies.filter(currency => currency.kind === null); + if (withoutKinds.length > 1) { + throw new Error( + `Only one currency can be without kind. Found ${withoutKinds.length}`, + ); + } +}