Merge pull request #3147 from spotify/ryanv/cost-insights/migrate-non-type-exports
Migrate type utilities for Cost Insights
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
migrate type utilities out of type definition files
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -15,4 +15,3 @@
|
||||
*/
|
||||
|
||||
export * from './CostInsightsApi';
|
||||
export * from './ExampleCostInsightsClient';
|
||||
|
||||
+10
-6
@@ -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;
|
||||
@@ -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],
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
getResetState,
|
||||
DefaultLoadingAction,
|
||||
getResetStateWithoutInitial,
|
||||
} from '../../types';
|
||||
} from '../../utils/loading';
|
||||
|
||||
type CostInsightsPageLoadingProps = {
|
||||
loadingActions: Array<string>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<AlertCost>;
|
||||
}
|
||||
|
||||
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 <ProjectGrowthAlertCard alert={this.data} />;
|
||||
}
|
||||
}
|
||||
|
||||
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 <UnlabeledDataflowAlertCard alert={this.data} />;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UnlabeledDataflowAlertProject {
|
||||
id: string;
|
||||
unlabeledCost: number;
|
||||
@@ -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 <CostGrowth /> 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 <CostOverviewCard /> 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { Maybe } from './Maybe';
|
||||
import { Duration } from './Duration';
|
||||
import { Group } from './Group';
|
||||
|
||||
export interface PageFilters {
|
||||
group: Maybe<string>;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,50 +15,3 @@
|
||||
*/
|
||||
|
||||
export type Loading = Record<string, boolean>;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -13,33 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type Maybe<T> = T | null;
|
||||
|
||||
export function notEmpty<TValue>(
|
||||
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<T>(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<T>(
|
||||
collection: T[],
|
||||
callback: (el: T) => boolean,
|
||||
): T {
|
||||
return assertAlways(collection.find(callback));
|
||||
}
|
||||
|
||||
@@ -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 <UnlabeledDataflowAlertCard alert={this.data} />;
|
||||
}
|
||||
}
|
||||
|
||||
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 <ProjectGrowthAlertCard alert={this.data} />;
|
||||
}
|
||||
}
|
||||
@@ -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<TValue>(
|
||||
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<T>(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<T>(
|
||||
collection: T[],
|
||||
callback: (el: T) => boolean,
|
||||
): T {
|
||||
return assertAlways(collection.find(callback));
|
||||
}
|
||||
@@ -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 <CostGrowth /> 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 <CostOverviewCard /> 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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
+2
-1
@@ -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';
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<T> = (alert: T) => T;
|
||||
type mockEntityRenderer<T> = (entity: T) => T;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user