diff --git a/.changeset/cost-insights-kind-trees-occur.md b/.changeset/cost-insights-kind-trees-occur.md new file mode 100644 index 0000000000..600cc3b3b4 --- /dev/null +++ b/.changeset/cost-insights-kind-trees-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +migrate type utilities out of type definition files diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 8c2fd11761..3b6e1e4fd5 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -106,7 +106,7 @@ costInsights: ## 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/spotify/backstage/blob/master/plugins/cost-insights/src/types/Alert.tsx). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations. +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/spotify/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. The Alert type includes an `element` field to supply the JSX Element that will be rendered in the Cost Insights "Action Items" section; we recommend using Backstage's [InfoCard](https://backstage.io/storybook/?path=/story/layout-information-card--default) and [Recharts](http://recharts.org/en-US/) to show actionable visualizations. diff --git a/plugins/cost-insights/dev/index.tsx b/plugins/cost-insights/dev/index.tsx index 836aea0edb..fe208d5bb8 100644 --- a/plugins/cost-insights/dev/index.tsx +++ b/plugins/cost-insights/dev/index.tsx @@ -15,8 +15,8 @@ */ import { createDevApp } from '@backstage/dev-utils'; import { createPlugin, createApiFactory } from '@backstage/core'; -import { ExampleCostInsightsClient } from '../src/api'; -import { costInsightsApiRef } from '../src'; +import { costInsightsApiRef } from '../src/api'; +import { ExampleCostInsightsClient } from '../src/client'; import { pluginConfig } from '../src/plugin'; const devPlugin = createPlugin({ diff --git a/plugins/cost-insights/src/api/index.ts b/plugins/cost-insights/src/api/index.ts index a367df9487..d231570e9b 100644 --- a/plugins/cost-insights/src/api/index.ts +++ b/plugins/cost-insights/src/api/index.ts @@ -15,4 +15,3 @@ */ export * from './CostInsightsApi'; -export * from './ExampleCostInsightsClient'; diff --git a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts b/plugins/cost-insights/src/client.ts similarity index 98% rename from plugins/cost-insights/src/api/ExampleCostInsightsClient.ts rename to plugins/cost-insights/src/client.ts index 35bff86f7f..f7671bcce0 100644 --- a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts +++ b/plugins/cost-insights/src/client.ts @@ -17,7 +17,7 @@ import dayjs from 'dayjs'; import regression, { DataPoint } from 'regression'; -import { CostInsightsApi } from './CostInsightsApi'; +import { CostInsightsApi } from '../src/api'; import { Alert, ChangeStatistic, @@ -25,19 +25,23 @@ import { DateAggregation, DEFAULT_DATE_FORMAT, Duration, - exclusiveEndDateOf, Group, - inclusiveStartDateOf, Maybe, MetricData, ProductCost, Project, - ProjectGrowthAlert, ProjectGrowthData, Trendline, - UnlabeledDataflowAlert, UnlabeledDataflowData, -} from '../types'; +} from '../src/types'; +import { + ProjectGrowthAlert, + UnlabeledDataflowAlert, +} from '../src/utils/alerts'; +import { + exclusiveEndDateOf, + inclusiveStartDateOf, +} from '../src/utils/duration'; type IntervalFields = { duration: Duration; diff --git a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx b/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx index 3e2ca4f8c8..d896bf0d47 100644 --- a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx +++ b/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx @@ -17,8 +17,9 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { AlertActionCard } from './AlertActionCard'; -import { ProjectGrowthAlert, ProjectGrowthData } from '../../types'; import { MockScrollProvider } from '../../utils/tests'; +import { ProjectGrowthAlert } from '../../utils/alerts'; +import { ProjectGrowthData } from '../../types'; const data: ProjectGrowthData = { aggregation: [500000.8, 970502.8], diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.tsx index 65e7e39c64..aa1fd5c601 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.tsx @@ -36,11 +36,11 @@ import { currencyFormatter } from '../../utils/formatters'; import { BarChartData, Maybe, - notEmpty, ResourceData, DataKey, CostInsightsTheme, } from '../../types'; +import { notEmpty } from '../../utils/assert'; import { useBarChartStyles } from '../../utils/styles'; import { resourceSort } from '../../utils/sort'; diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index ed81f657c5..9c76d6d998 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -17,14 +17,10 @@ import React, { PropsWithChildren } from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { CostGrowth } from './CostGrowth'; -import { - defaultCurrencies, - Currency, - CurrencyType, - Duration, - findAlways, -} from '../../types'; +import { Currency, CurrencyType, Duration } from '../../types'; +import { findAlways } from '../../utils/assert'; import { MockConfigProvider, MockCurrencyProvider } from '../../utils/tests'; +import { defaultCurrencies } from '../../utils/currency'; const engineers = findAlways(defaultCurrencies, c => c.kind === null); const usd = findAlways(defaultCurrencies, c => c.kind === CurrencyType.USD); diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx index a43cae9720..89e44ee185 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx @@ -21,10 +21,10 @@ import { CurrencyType, Duration, EngineerThreshold, - Growth, - growthOf, - rateOf, + GrowthType, } from '../../types'; +import { rateOf } from '../../utils/currency'; +import { growthOf } from '../../utils/change'; import { useCostGrowthStyles as useStyles } from '../../utils/styles'; import { formatPercent, formatCurrency } from '../../utils/formatters'; import { indefiniteArticleOf } from '../../utils/grammar'; @@ -51,8 +51,8 @@ export const CostGrowth = ({ change, duration }: CostGrowthProps) => { // Determine if growth is significant enough to highlight const growth = growthOf(engineers, change.ratio); const classes = classnames({ - [styles.excess]: growth === Growth.Excess, - [styles.savings]: growth === Growth.Savings, + [styles.excess]: growth === GrowthType.Excess, + [styles.savings]: growth === GrowthType.Savings, }); const percent = formatPercent(ratio); diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx index f190fa88bb..810fc767a7 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx @@ -25,7 +25,7 @@ import { } from '@material-ui/core'; import { useNavigationStyles } from '../../utils/styles'; import { useConfig, useScroll } from '../../hooks'; -import { findAlways } from '../../types'; +import { findAlways } from '../../utils/assert'; import { DefaultNavigation, NavigationItem, diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index 52f70d2756..3234d1aa7f 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -41,16 +41,10 @@ import { useLastCompleteBillingDate, useLoading, } from '../../hooks'; -import { - Alert, - Cost, - intervalsOf, - Maybe, - MetricData, - Project, -} from '../../types'; +import { Alert, Cost, Maybe, MetricData, Project } from '../../types'; import { mapLoadingToProps } from './selector'; import { ProjectSelect } from '../ProjectSelect'; +import { intervalsOf } from '../../utils/duration'; import { useSubtleTypographyStyles } from '../../utils/styles'; export const CostInsightsPage = () => { diff --git a/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx b/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx index a73c0ff975..664c90726a 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx @@ -18,7 +18,7 @@ import { getResetState, DefaultLoadingAction, getResetStateWithoutInitial, -} from '../../types'; +} from '../../utils/loading'; type CostInsightsPageLoadingProps = { loadingActions: Array; diff --git a/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts b/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts index 3aa53cce21..35100338ac 100644 --- a/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts +++ b/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts @@ -16,7 +16,8 @@ import { MapFiltersToProps } from '../../hooks/useFilters'; import { MapLoadingToProps } from '../../hooks/useLoading'; -import { Group, PageFilters, getResetStateWithoutInitial } from '../../types'; +import { Group, PageFilters } from '../../types'; +import { getResetStateWithoutInitial } from '../../utils/loading'; type CostInsightsTabsFilterProps = PageFilters & { setGroup: (group: Group) => void; diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index c6269f7dc6..7d55026be8 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -26,13 +26,9 @@ import { useScroll, useFilters, useConfig } from '../../hooks'; import { mapFiltersToProps } from './selector'; import { DefaultNavigation } from '../../utils/navigation'; import { formatPercent } from '../../utils/formatters'; -import { - Cost, - CostInsightsTheme, - MetricData, - findAlways, - getComparedChange, -} from '../../types'; +import { findAlways } from '../../utils/assert'; +import { getComparedChange } from '../../utils/change'; +import { Cost, CostInsightsTheme, MetricData } from '../../types'; export type CostOverviewCardProps = { dailyCostData: Cost; diff --git a/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx b/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx index 413426e9af..1b5920ffcb 100644 --- a/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx +++ b/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx @@ -16,7 +16,8 @@ import React from 'react'; import { MenuItem, Select, SelectProps } from '@material-ui/core'; -import { Currency, CurrencyType, findAlways } from '../../types'; +import { Currency, CurrencyType } from '../../types'; +import { findAlways } from '../../utils/assert'; import { useSelectStyles as useStyles } from '../../utils/styles'; const NULL_VALUE = 'engineers'; diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx index e2cf1b7480..a3519ff61b 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx @@ -19,8 +19,9 @@ import { getByRole, waitFor } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; import UserEvent from '@testing-library/user-event'; import { PeriodSelect, getDefaultOptions } from './PeriodSelect'; -import { Duration, getDefaultPageFilters, Group } from '../../types'; +import { getDefaultPageFilters } from '../../utils/filters'; import { MockBillingDateProvider } from '../../utils/tests'; +import { Group, Duration } from '../../types'; const DefaultPageFilters = getDefaultPageFilters([{ id: 'tools' }] as Group[]); const lastCompleteBillingDate = '2020-05-01'; diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx index 07365c32eb..4908641d6f 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx @@ -16,11 +16,12 @@ import React from 'react'; import { MenuItem, Select, SelectProps } from '@material-ui/core'; +import { Duration } from '../../types'; import { formatLastTwoLookaheadQuarters, formatLastTwoMonths, } from '../../utils/formatters'; -import { Duration, findAlways } from '../../types'; +import { findAlways } from '../../utils/assert'; import { useSelectStyles as useStyles } from '../../utils/styles'; import { useLastCompleteBillingDate } from '../../hooks'; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts b/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts index 001ff717bd..efc8448034 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts +++ b/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts @@ -16,7 +16,8 @@ import { MapFiltersToProps } from '../../hooks/useFilters'; import { MapLoadingToProps } from '../../hooks/useLoading'; -import { Duration, PageFilters, ProductPeriod, findAlways } from '../../types'; +import { Duration, PageFilters, ProductPeriod } from '../../types'; +import { findAlways } from '../../utils/assert'; type ProductInsightsCardFilterProps = PageFilters & { product: ProductPeriod; diff --git a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx index 4b5da409a4..db3f2f9bf6 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx @@ -23,7 +23,9 @@ import { MockConfigProvider, MockBillingDateProvider, } from '../../utils/tests'; -import { AlertCost, defaultCurrencies, findAlways } from '../../types'; +import { AlertCost } from '../../types'; +import { defaultCurrencies } from '../../utils/currency'; +import { findAlways } from '../../utils/assert'; const engineers = findAlways(defaultCurrencies, c => c.kind === null); diff --git a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx index 3c2d241222..f2083388ae 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx @@ -23,9 +23,9 @@ import { Duration, Entity, Product, - ProjectGrowthAlert, ProjectGrowthData, } from '../../types'; +import { ProjectGrowthAlert } from '../../utils/alerts'; import { ResourceGrowthBarChartLegend } from '../ResourceGrowthBarChartLegend'; import { ResourceGrowthBarChart } from '../ResourceGrowthBarChart'; diff --git a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx index 2bc998d9df..47285a2363 100644 --- a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx +++ b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx @@ -17,8 +17,10 @@ import React, { PropsWithChildren } from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { ResourceGrowthBarChartLegend } from './ResourceGrowthBarChartLegend'; -import { defaultCurrencies, Duration, findAlways } from '../../types'; +import { defaultCurrencies } from '../../utils/currency'; +import { findAlways } from '../../utils/assert'; import { MockConfigProvider, MockCurrencyProvider } from '../../utils/tests'; +import { Duration } from '../../types'; const engineers = findAlways(defaultCurrencies, c => c.kind === null); diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index f63b9228a9..450ab545ac 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -23,9 +23,10 @@ import React, { } from 'react'; import { configApiRef, useApi } from '@backstage/core'; import { Config as BackstageConfig } from '@backstage/config'; -import { Currency, defaultCurrencies, Icon, Metric, Product } from '../types'; +import { Currency, Icon, Metric, Product } from '../types'; import { getIcon } from '../utils/navigation'; import { validateMetrics } from '../utils/config'; +import { defaultCurrencies } from '../utils/currency'; /* * Config schema 2020-10-15 diff --git a/plugins/cost-insights/src/hooks/useCurrency.tsx b/plugins/cost-insights/src/hooks/useCurrency.tsx index 34a4f54d5f..ff41b033e4 100644 --- a/plugins/cost-insights/src/hooks/useCurrency.tsx +++ b/plugins/cost-insights/src/hooks/useCurrency.tsx @@ -20,7 +20,9 @@ import React, { useContext, PropsWithChildren, } from 'react'; -import { Currency, defaultCurrencies, findAlways } from '../types'; +import { Currency } from '../types'; +import { findAlways } from '../utils/assert'; +import { defaultCurrencies } from '../utils/currency'; export type CurrencyContextProps = { currency: Currency; diff --git a/plugins/cost-insights/src/hooks/useGroups.tsx b/plugins/cost-insights/src/hooks/useGroups.tsx index 13cec25017..50444f2915 100644 --- a/plugins/cost-insights/src/hooks/useGroups.tsx +++ b/plugins/cost-insights/src/hooks/useGroups.tsx @@ -24,7 +24,8 @@ import { Alert } from '@material-ui/lab'; import { useApi, identityApiRef } from '@backstage/core'; import { costInsightsApiRef } from '../api'; import { MapLoadingToProps, useLoading } from './useLoading'; -import { DefaultLoadingAction, Group, Maybe } from '../types'; +import { Group, Maybe } from '../types'; +import { DefaultLoadingAction } from '../utils/loading'; type GroupsProviderLoadingProps = { dispatchLoadingGroups: (isLoading: boolean) => void; diff --git a/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx b/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx index 33d037267d..d2ee43980f 100644 --- a/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx +++ b/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx @@ -25,7 +25,8 @@ import { Alert } from '@material-ui/lab'; import { useApi } from '@backstage/core'; import { costInsightsApiRef } from '../api'; import { MapLoadingToProps, useLoading } from './useLoading'; -import { DefaultLoadingAction, Maybe } from '../types'; +import { DefaultLoadingAction } from '../utils/loading'; +import { Maybe } from '../types'; type BillingDateProviderLoadingProps = { dispatchLoadingBillingDate: (isLoading: boolean) => void; diff --git a/plugins/cost-insights/src/hooks/useLoading.tsx b/plugins/cost-insights/src/hooks/useLoading.tsx index 31402ba0d1..420ac7ed56 100644 --- a/plugins/cost-insights/src/hooks/useLoading.tsx +++ b/plugins/cost-insights/src/hooks/useLoading.tsx @@ -26,12 +26,12 @@ import React, { useState, } from 'react'; import { Backdrop, CircularProgress } from '@material-ui/core'; +import { Loading } from '../types'; import { DefaultLoadingAction, getDefaultState, getLoadingActions, - Loading, -} from '../types'; +} from '../utils/loading'; import { useBackdropStyles as useStyles } from '../utils/styles'; import { useConfig } from './useConfig'; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 71cc66dba6..1e6172d568 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -15,8 +15,10 @@ */ export { plugin } from './plugin'; +export * from './client'; export * from './api'; export * from './components'; export { useCurrency } from './hooks'; export * from './types'; export * from './utils/tests'; +export * from './utils/duration'; diff --git a/plugins/cost-insights/src/types/Alert.tsx b/plugins/cost-insights/src/types/Alert.ts similarity index 59% rename from plugins/cost-insights/src/types/Alert.tsx rename to plugins/cost-insights/src/types/Alert.ts index 4ed55a844c..d6046e3241 100644 --- a/plugins/cost-insights/src/types/Alert.tsx +++ b/plugins/cost-insights/src/types/Alert.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import React from 'react'; import { ChangeStatistic } from './ChangeStatistic'; import { Maybe } from './Maybe'; -import { UnlabeledDataflowAlertCard } from '../components/UnlabeledDataflowAlertCard'; -import { ProjectGrowthAlertCard } from '../components/ProjectGrowthAlertCard'; /** * Generic alert type with required fields for display. The `element` field will be rendered in @@ -57,11 +54,6 @@ export enum DataKey { Name = 'name', } -/** - * The alerts below are examples of Alert implementation; the CostInsightsApi permits returning - * any implementation of the Alert type, so adopters can create their own. The CostInsightsApi - * fetches alert data from the backend, then creates Alert classes with the data. - */ export interface ProjectGrowthData { project: string; periodStart: string; @@ -71,26 +63,6 @@ export interface ProjectGrowthData { products: Array; } -export class ProjectGrowthAlert implements Alert { - data: ProjectGrowthData; - - constructor(data: ProjectGrowthData) { - this.data = data; - } - - get title() { - return `Investigate cost growth in project ${this.data.project}`; - } - - subtitle = - 'Cost growth outpacing business growth is unsustainable long-term.'; - url = '/cost-insights/investigating-growth'; - - get element() { - return ; - } -} - export interface UnlabeledDataflowData { periodStart: string; periodEnd: string; @@ -99,23 +71,6 @@ export interface UnlabeledDataflowData { labeledCost: number; } -export class UnlabeledDataflowAlert implements Alert { - data: UnlabeledDataflowData; - - constructor(data: UnlabeledDataflowData) { - this.data = data; - } - - title = 'Add labels to workflows'; - subtitle = - 'Labels show in billing data, enabling cost insights for each workflow.'; - url = '/cost-insights/labeling-jobs'; - - get element() { - return ; - } -} - export interface UnlabeledDataflowAlertProject { id: string; unlabeledCost: number; diff --git a/plugins/cost-insights/src/types/ChangeStatistic.ts b/plugins/cost-insights/src/types/ChangeStatistic.ts index cdd2e02da6..a47640411a 100644 --- a/plugins/cost-insights/src/types/ChangeStatistic.ts +++ b/plugins/cost-insights/src/types/ChangeStatistic.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { Cost } from './Cost'; -import { MetricData } from './MetricData'; -import { aggregationSort } from '../utils/sort'; - export interface ChangeStatistic { // The ratio of change from one duration to another, expressed as: (newSum - oldSum) / oldSum ratio: number; @@ -32,34 +28,8 @@ export enum ChangeThreshold { lower = -0.05, } -export enum Growth { +export enum GrowthType { Negligible, Savings, Excess, } - -// Used by for displaying status colors -export function growthOf(amount: number, ratio: number) { - if (amount >= EngineerThreshold && ratio >= ChangeThreshold.upper) { - return Growth.Excess; - } - - if (amount >= EngineerThreshold && ratio <= ChangeThreshold.lower) { - return Growth.Savings; - } - - return Growth.Negligible; -} - -// Used by for displaying engineer totals -export function getComparedChange( - dailyCost: Cost, - metricData: MetricData, -): ChangeStatistic { - const ratio = dailyCost.change.ratio - metricData.change.ratio; - const amount = dailyCost.aggregation.slice().sort(aggregationSort)[0].amount; - return { - ratio: ratio, - amount: amount * ratio, - }; -} diff --git a/plugins/cost-insights/src/types/Currency.ts b/plugins/cost-insights/src/types/Currency.ts index 633e405893..8a8af81b0e 100644 --- a/plugins/cost-insights/src/types/Currency.ts +++ b/plugins/cost-insights/src/types/Currency.ts @@ -13,15 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Duration } from './Duration'; -import { assertNever } from './Maybe'; - -export enum CurrencyType { - USD = 'USD', - CarbonOffsetTons = 'CARBON_OFFSET_TONS', - Beers = 'BEERS', - IceCream = 'PINTS_OF_ICE_CREAM', -} export interface Currency { kind: string | null; @@ -31,48 +22,9 @@ export interface Currency { rate?: number; } -export const rateOf = (cost: number, duration: Duration) => { - switch (duration) { - case Duration.P1M: - case Duration.P30D: - return cost / 12; - case Duration.P90D: - case Duration.P3M: - return cost / 4; - default: - return assertNever(duration); - } -}; - -export const defaultCurrencies: Currency[] = [ - { - kind: null, - label: 'Engineers 🛠', - unit: 'engineer', - }, - { - kind: CurrencyType.USD, - label: 'US Dollars 💵', - unit: 'dollar', - prefix: '$', - rate: 1, - }, - { - kind: CurrencyType.CarbonOffsetTons, - label: 'Carbon Offset Tons ♻️⚖️s', - unit: 'carbon offset ton', - rate: 3.5, - }, - { - kind: CurrencyType.Beers, - label: 'Beers 🍺', - unit: 'beer', - rate: 4.5, - }, - { - kind: CurrencyType.IceCream, - label: 'Pints of Ice Cream 🍦', - unit: 'ice cream pint', - rate: 5.5, - }, -]; +export enum CurrencyType { + USD = 'USD', + CarbonOffsetTons = 'CARBON_OFFSET_TONS', + Beers = 'BEERS', + IceCream = 'PINTS_OF_ICE_CREAM', +} diff --git a/plugins/cost-insights/src/types/Duration.ts b/plugins/cost-insights/src/types/Duration.ts index 72501d46d5..acf707dd1e 100644 --- a/plugins/cost-insights/src/types/Duration.ts +++ b/plugins/cost-insights/src/types/Duration.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -import moment from 'moment'; -import { assertNever } from './Maybe'; - /** * Time periods for cost comparison; slight abuse of ISO 8601 periods. We take P1M and P3M to mean * 'last completed [month|quarter]', and P30D/P90D to be '[month|quarter] relative to today'. So if @@ -31,72 +28,3 @@ export enum Duration { } export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; - -/** - * Derive the start date of a given period, assuming two repeating intervals. - * - * @param duration see comment on Duration enum - * @param endDate from CostInsightsApi.getLastCompleteBillingDate - */ -export function inclusiveStartDateOf( - duration: Duration, - endDate: string, -): string { - switch (duration) { - case Duration.P30D: - case Duration.P90D: - return moment(endDate) - .utc() - .subtract(moment.duration(duration).add(moment.duration(duration))) - .format(DEFAULT_DATE_FORMAT); - case Duration.P1M: - return moment(endDate) - .utc() - .startOf('month') - .subtract(moment.duration(duration).add(moment.duration(duration))) - .format(DEFAULT_DATE_FORMAT); - case Duration.P3M: - return moment(endDate) - .utc() - .startOf('quarter') - .subtract(moment.duration(duration).add(moment.duration(duration))) - .format(DEFAULT_DATE_FORMAT); - default: - return assertNever(duration); - } -} - -export function exclusiveEndDateOf( - duration: Duration, - endDate: string, -): string { - switch (duration) { - case Duration.P30D: - case Duration.P90D: - return moment(endDate).utc().add(1, 'day').format(DEFAULT_DATE_FORMAT); - case Duration.P1M: - return moment(endDate).utc().startOf('month').format(DEFAULT_DATE_FORMAT); - case Duration.P3M: - return moment(endDate) - .utc() - .startOf('quarter') - .format(DEFAULT_DATE_FORMAT); - default: - return assertNever(duration); - } -} - -export function inclusiveEndDateOf( - duration: Duration, - endDate: string, -): string { - return moment(exclusiveEndDateOf(duration, endDate)) - .utc() - .subtract(1, 'day') - .format(DEFAULT_DATE_FORMAT); -} - -// https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals -export function intervalsOf(duration: Duration, endDate: string) { - return `R2/${duration}/${exclusiveEndDateOf(duration, endDate)}`; -} diff --git a/plugins/cost-insights/src/types/Filters.ts b/plugins/cost-insights/src/types/Filters.ts index b1fbbb2848..120738fcc7 100644 --- a/plugins/cost-insights/src/types/Filters.ts +++ b/plugins/cost-insights/src/types/Filters.ts @@ -16,7 +16,6 @@ import { Maybe } from './Maybe'; import { Duration } from './Duration'; -import { Group } from './Group'; export interface PageFilters { group: Maybe; @@ -31,12 +30,3 @@ export interface ProductPeriod { duration: Duration; productType: string; } - -export function getDefaultPageFilters(groups: Group[]): PageFilters { - return { - group: groups.length ? groups[0].id : null, - project: null, - duration: Duration.P90D, - metric: null, - }; -} diff --git a/plugins/cost-insights/src/types/Loading.ts b/plugins/cost-insights/src/types/Loading.ts index 8bc0c4eaef..741453369c 100644 --- a/plugins/cost-insights/src/types/Loading.ts +++ b/plugins/cost-insights/src/types/Loading.ts @@ -15,50 +15,3 @@ */ export type Loading = Record; - -export enum DefaultLoadingAction { - UserGroups = 'user-groups', - LastCompleteBillingDate = 'billing-date', - CostInsightsInitial = 'cost-insights-initial', - CostInsightsPage = 'cost-insights-page', -} - -export const INITIAL_LOADING_ACTIONS = [ - DefaultLoadingAction.UserGroups, - DefaultLoadingAction.CostInsightsInitial, -]; - -export const getDefaultState = (loadingActions: string[]): Loading => { - return loadingActions.reduce( - (defaultState, action) => ({ ...defaultState, [action]: true }), - {}, - ); -}; - -export const getResetState = (loadingActions: string[]): Loading => { - return loadingActions.reduce( - (defaultState, action) => ({ ...defaultState, [action]: false }), - {}, - ); -}; - -export const getResetStateWithoutInitial = ( - loadingActions: string[], -): Loading => { - return loadingActions.reduce((defaultState, action) => { - const loadingActionState = (INITIAL_LOADING_ACTIONS as string[]).includes( - action, - ) - ? false - : true; - return { ...defaultState, [action]: loadingActionState }; - }, {}); -}; - -export function getLoadingActions(products: string[]): string[] { - return ([ - DefaultLoadingAction.UserGroups, - DefaultLoadingAction.CostInsightsInitial, - DefaultLoadingAction.CostInsightsPage, - ] as string[]).concat(products); -} diff --git a/plugins/cost-insights/src/types/Maybe.ts b/plugins/cost-insights/src/types/Maybe.ts index d721f28d75..a01a658382 100644 --- a/plugins/cost-insights/src/types/Maybe.ts +++ b/plugins/cost-insights/src/types/Maybe.ts @@ -13,33 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export type Maybe = T | null; - -export function notEmpty( - value: TValue | null | undefined, -): value is TValue { - return value !== null && value !== undefined; -} - -// Utility for exhaustiveness checking in switch statements -export function assertNever(x: never): never { - throw new Error(`Exhaustiveness check failed: ${x}`); -} - -export function assertAlways(argument: T | undefined): T { - if (argument === undefined) { - throw new TypeError( - 'Expected to always find a value but received undefined', - ); - } - return argument; -} - -// Utility for working with static lists; asserts a value will always be found or -// throws an error -export function findAlways( - collection: T[], - callback: (el: T) => boolean, -): T { - return assertAlways(collection.find(callback)); -} diff --git a/plugins/cost-insights/src/utils/alerts.tsx b/plugins/cost-insights/src/utils/alerts.tsx new file mode 100644 index 0000000000..fbb148f38a --- /dev/null +++ b/plugins/cost-insights/src/utils/alerts.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 React from 'react'; +import { Alert, UnlabeledDataflowData, ProjectGrowthData } from '../types'; +import { UnlabeledDataflowAlertCard } from '../components/UnlabeledDataflowAlertCard'; +import { ProjectGrowthAlertCard } from '../components/ProjectGrowthAlertCard'; + +/** + * The alerts below are examples of Alert implementation; the CostInsightsApi permits returning + * any implementation of the Alert type, so adopters can create their own. The CostInsightsApi + * fetches alert data from the backend, then creates Alert classes with the data. + */ + +export class UnlabeledDataflowAlert implements Alert { + data: UnlabeledDataflowData; + + constructor(data: UnlabeledDataflowData) { + this.data = data; + } + + title = 'Add labels to workflows'; + subtitle = + 'Labels show in billing data, enabling cost insights for each workflow.'; + url = '/cost-insights/labeling-jobs'; + + get element() { + return ; + } +} + +export class ProjectGrowthAlert implements Alert { + data: ProjectGrowthData; + + constructor(data: ProjectGrowthData) { + this.data = data; + } + + get title() { + return `Investigate cost growth in project ${this.data.project}`; + } + + subtitle = + 'Cost growth outpacing business growth is unsustainable long-term.'; + url = '/cost-insights/investigating-growth'; + + get element() { + return ; + } +} diff --git a/plugins/cost-insights/src/utils/assert.ts b/plugins/cost-insights/src/utils/assert.ts new file mode 100644 index 0000000000..8f7caef0fa --- /dev/null +++ b/plugins/cost-insights/src/utils/assert.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export function notEmpty( + value: TValue | null | undefined, +): value is TValue { + return value !== null && value !== undefined; +} + +// Utility for exhaustiveness checking in switch statements +export function assertNever(x: never): never { + throw new Error(`Exhaustiveness check failed: ${x}`); +} + +export function assertAlways(argument: T | undefined): T { + if (argument === undefined) { + throw new TypeError( + 'Expected to always find a value but received undefined', + ); + } + return argument; +} + +// Utility for working with static lists; asserts a value will always be found or +// throws an error +export function findAlways( + collection: T[], + callback: (el: T) => boolean, +): T { + return assertAlways(collection.find(callback)); +} diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts new file mode 100644 index 0000000000..1fe3662537 --- /dev/null +++ b/plugins/cost-insights/src/utils/change.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + Cost, + ChangeStatistic, + ChangeThreshold, + EngineerThreshold, + GrowthType, + MetricData, +} from '../types'; +import { aggregationSort } from '../utils/sort'; + +// Used by for displaying status colors +export function growthOf(amount: number, ratio: number) { + if (amount >= EngineerThreshold && ratio >= ChangeThreshold.upper) { + return GrowthType.Excess; + } + + if (amount >= EngineerThreshold && ratio <= ChangeThreshold.lower) { + return GrowthType.Savings; + } + + return GrowthType.Negligible; +} + +// Used by for displaying engineer totals +export function getComparedChange( + dailyCost: Cost, + metricData: MetricData, +): ChangeStatistic { + const ratio = dailyCost.change.ratio - metricData.change.ratio; + const amount = dailyCost.aggregation.slice().sort(aggregationSort)[0].amount; + return { + ratio: ratio, + amount: amount * ratio, + }; +} diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts new file mode 100644 index 0000000000..663a29a112 --- /dev/null +++ b/plugins/cost-insights/src/utils/currency.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Currency, CurrencyType, Duration } from '../types'; +import { assertNever } from '../utils/assert'; + +export const rateOf = (cost: number, duration: Duration) => { + switch (duration) { + case Duration.P1M: + case Duration.P30D: + return cost / 12; + case Duration.P90D: + case Duration.P3M: + return cost / 4; + default: + return assertNever(duration); + } +}; + +export const defaultCurrencies: Currency[] = [ + { + kind: null, + label: 'Engineers 🛠', + unit: 'engineer', + }, + { + kind: CurrencyType.USD, + label: 'US Dollars 💵', + unit: 'dollar', + prefix: '$', + rate: 1, + }, + { + kind: CurrencyType.CarbonOffsetTons, + label: 'Carbon Offset Tons ♻️⚖️s', + unit: 'carbon offset ton', + rate: 3.5, + }, + { + kind: CurrencyType.Beers, + label: 'Beers 🍺', + unit: 'beer', + rate: 4.5, + }, + { + kind: CurrencyType.IceCream, + label: 'Pints of Ice Cream 🍦', + unit: 'ice cream pint', + rate: 5.5, + }, +]; diff --git a/plugins/cost-insights/src/types/Duration.test.ts b/plugins/cost-insights/src/utils/duration.test.ts similarity index 92% rename from plugins/cost-insights/src/types/Duration.test.ts rename to plugins/cost-insights/src/utils/duration.test.ts index 4ab84de974..a5509eda07 100644 --- a/plugins/cost-insights/src/types/Duration.test.ts +++ b/plugins/cost-insights/src/utils/duration.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Duration, inclusiveEndDateOf, inclusiveStartDateOf } from './Duration'; +import { Duration } from '../types'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; const lastCompleteBillingDate = '2020-06-05'; diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts new file mode 100644 index 0000000000..6a880b382c --- /dev/null +++ b/plugins/cost-insights/src/utils/duration.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 moment from 'moment'; +import { Duration, DEFAULT_DATE_FORMAT } from '../types'; +import { assertNever } from './assert'; + +/** + * Derive the start date of a given period, assuming two repeating intervals. + * + * @param duration see comment on Duration enum + * @param endDate from CostInsightsApi.getLastCompleteBillingDate + */ +export function inclusiveStartDateOf( + duration: Duration, + endDate: string, +): string { + switch (duration) { + case Duration.P30D: + case Duration.P90D: + return moment(endDate) + .utc() + .subtract(moment.duration(duration).add(moment.duration(duration))) + .format(DEFAULT_DATE_FORMAT); + case Duration.P1M: + return moment(endDate) + .utc() + .startOf('month') + .subtract(moment.duration(duration).add(moment.duration(duration))) + .format(DEFAULT_DATE_FORMAT); + case Duration.P3M: + return moment(endDate) + .utc() + .startOf('quarter') + .subtract(moment.duration(duration).add(moment.duration(duration))) + .format(DEFAULT_DATE_FORMAT); + default: + return assertNever(duration); + } +} + +export function exclusiveEndDateOf( + duration: Duration, + endDate: string, +): string { + switch (duration) { + case Duration.P30D: + case Duration.P90D: + return moment(endDate).utc().add(1, 'day').format(DEFAULT_DATE_FORMAT); + case Duration.P1M: + return moment(endDate).utc().startOf('month').format(DEFAULT_DATE_FORMAT); + case Duration.P3M: + return moment(endDate) + .utc() + .startOf('quarter') + .format(DEFAULT_DATE_FORMAT); + default: + return assertNever(duration); + } +} + +export function inclusiveEndDateOf( + duration: Duration, + endDate: string, +): string { + return moment(exclusiveEndDateOf(duration, endDate)) + .utc() + .subtract(1, 'day') + .format(DEFAULT_DATE_FORMAT); +} + +// https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals +export function intervalsOf(duration: Duration, endDate: string) { + return `R2/${duration}/${exclusiveEndDateOf(duration, endDate)}`; +} diff --git a/plugins/cost-insights/src/utils/filters.ts b/plugins/cost-insights/src/utils/filters.ts new file mode 100644 index 0000000000..fbe1bf46cc --- /dev/null +++ b/plugins/cost-insights/src/utils/filters.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Duration, Group, PageFilters } from '../types'; + +export function getDefaultPageFilters(groups: Group[]): PageFilters { + return { + group: groups.length ? groups[0].id : null, + project: null, + duration: Duration.P90D, + metric: null, + }; +} diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 44c7517351..bf8e26ef5f 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -15,11 +15,8 @@ */ import moment from 'moment'; -import { - Duration, - inclusiveEndDateOf, - inclusiveStartDateOf, -} from '../types/Duration'; +import { Duration } from '../types'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; import { pluralOf } from '../utils/grammar'; export type Period = { diff --git a/plugins/cost-insights/src/utils/history.ts b/plugins/cost-insights/src/utils/history.ts index 90df574acf..7d45cfdad2 100644 --- a/plugins/cost-insights/src/utils/history.ts +++ b/plugins/cost-insights/src/utils/history.ts @@ -16,7 +16,8 @@ import qs from 'qs'; import * as yup from 'yup'; -import { Group, PageFilters, getDefaultPageFilters, Duration } from '../types'; +import { Duration, Group, PageFilters } from '../types'; +import { getDefaultPageFilters } from '../utils/filters'; import { ConfigContextProps } from '../hooks/useConfig'; const schema = yup diff --git a/plugins/cost-insights/src/utils/loading.ts b/plugins/cost-insights/src/utils/loading.ts new file mode 100644 index 0000000000..4652603926 --- /dev/null +++ b/plugins/cost-insights/src/utils/loading.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Loading } from '../types'; + +export enum DefaultLoadingAction { + UserGroups = 'user-groups', + LastCompleteBillingDate = 'billing-date', + CostInsightsInitial = 'cost-insights-initial', + CostInsightsPage = 'cost-insights-page', +} + +export const INITIAL_LOADING_ACTIONS = [ + DefaultLoadingAction.UserGroups, + DefaultLoadingAction.CostInsightsInitial, +]; + +export const getDefaultState = (loadingActions: string[]): Loading => { + return loadingActions.reduce( + (defaultState, action) => ({ ...defaultState, [action]: true }), + {}, + ); +}; + +export const getResetState = (loadingActions: string[]): Loading => { + return loadingActions.reduce( + (defaultState, action) => ({ ...defaultState, [action]: false }), + {}, + ); +}; + +export const getResetStateWithoutInitial = ( + loadingActions: string[], +): Loading => { + return loadingActions.reduce((defaultState, action) => { + const loadingActionState = (INITIAL_LOADING_ACTIONS as string[]).includes( + action, + ) + ? false + : true; + return { ...defaultState, [action]: loadingActionState }; + }, {}); +}; + +export function getLoadingActions(products: string[]): string[] { + return ([ + DefaultLoadingAction.UserGroups, + DefaultLoadingAction.CostInsightsInitial, + DefaultLoadingAction.CostInsightsPage, + ] as string[]).concat(products); +} diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 11c70eee25..58ba4a4251 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -14,12 +14,11 @@ * limitations under the License. */ +import { Config } from '@backstage/config'; +import { ConfigApi } from '@backstage/core'; import { - DefaultLoadingAction, Duration, Entity, - findAlways, - getDefaultState as getDefaultLoadingState, Product, ProductCost, ProductFilters, @@ -27,8 +26,11 @@ import { UnlabeledDataflowAlertProject, UnlabeledDataflowData, } from '../types'; -import { Config } from '@backstage/config'; -import { ConfigApi } from '@backstage/core'; +import { + DefaultLoadingAction, + getDefaultState as getDefaultLoadingState, +} from '../utils/loading'; +import { findAlways } from '../utils/assert'; type mockAlertRenderer = (alert: T) => T; type mockEntityRenderer = (entity: T) => T; diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/utils/tests.tsx index db47bd72bd..852330acc3 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/utils/tests.tsx @@ -32,7 +32,7 @@ import { BillingDateContextProps, } from '../hooks/useLastCompleteBillingDate'; import { ScrollContext, ScrollContextProps } from '../hooks/useScroll'; -import { Duration, Group } from '../types'; +import { Group, Duration } from '../types'; /* Mock Providers and types are exposed publicly to allow users to test custom implementations