Merge pull request #4786 from backstage/cost-insights-migrate-example

Cost insights migrate example
This commit is contained in:
Ryan Vazquez
2021-03-03 16:20:18 -05:00
committed by GitHub
44 changed files with 660 additions and 455 deletions
+1 -1
View File
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { ExampleCostInsightsClient } from '../src/example';
import { costInsightsApiRef } from '../src/api';
import { ExampleCostInsightsClient } from '../src/client';
import { costInsightsPlugin } from '../src/plugin';
createDevApp()
@@ -23,7 +23,7 @@ import {
MockCurrencyProvider,
MockConfigProvider,
MockBillingDateProvider,
} from '../utils/tests';
} from '../testUtils';
const mockData: ProjectGrowthData = {
project: 'test-project',
@@ -23,7 +23,7 @@ import {
MockCurrencyProvider,
MockConfigProvider,
MockBillingDateProvider,
} from '../utils/tests';
} from '../testUtils';
const mockData: UnlabeledDataflowData = {
periodStart: '2021-02-01',
@@ -16,6 +16,3 @@
export { ProjectGrowthAlert } from './ProjectGrowthAlert';
export { UnlabeledDataflowAlert } from './UnlabeledDataflowAlert';
export { KubernetesMigrationAlert } from './KubernetesMigrationAlert';
export type { MigrationAlert } from './KubernetesMigrationAlert';
export type { MigrationData } from './KubernetesMigrationAlert';
@@ -17,7 +17,7 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ActionItemCard } from './ActionItemCard';
import { MockScrollProvider } from '../../utils/tests';
import { MockScrollProvider } from '../../testUtils';
import { ProjectGrowthAlert } from '../../alerts';
import { ProjectGrowthData } from '../../types';
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { ActionItems } from './ActionItems';
import { MockScrollProvider } from '../../utils/tests';
import { MockScrollProvider } from '../../testUtils';
function renderInContext(children: JSX.Element) {
return render(<MockScrollProvider>{children}</MockScrollProvider>);
@@ -17,7 +17,7 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { AlertInsights } from './AlertInsights';
import { MockScrollProvider, MockLoadingProvider } from '../../utils/tests';
import { MockScrollProvider, MockLoadingProvider } from '../../testUtils';
function renderInContext(children: JSX.Element) {
return render(
@@ -17,7 +17,7 @@ import React from 'react';
import { AlertInsightsSection } from './AlertInsightsSection';
import { render } from '@testing-library/react';
import { Alert } from '../../types';
import { MockScrollProvider } from '../../utils/tests';
import { MockScrollProvider } from '../../testUtils';
const mockAlert: Alert = {
subtitle:
@@ -17,7 +17,7 @@ import React from 'react';
import { AlertStatusSummary } from './AlertStatusSummary';
import { render } from '@testing-library/react';
import { Alert, AlertStatus } from '../../types';
import { MockScrollProvider } from '../../utils/tests';
import { MockScrollProvider } from '../../testUtils';
const mockSnoozed: Alert = {
title: 'snoozed-title',
@@ -18,7 +18,7 @@ import React from 'react';
import { fireEvent } from '@testing-library/react';
import { BarChart, BarChartProps } from './BarChart';
import { ResourceData } from '../../types';
import { createMockEntity } from '../../utils/mockData';
import { createMockEntity } from '../../testUtils';
import { resourceSort } from '../../utils/sort';
import { renderInTestApp } from '@backstage/test-utils';
@@ -19,7 +19,7 @@ import { renderInTestApp } from '@backstage/test-utils';
import { CostGrowth } from './CostGrowth';
import { Currency, CurrencyType, Duration } from '../../types';
import { findAlways } from '../../utils/assert';
import { MockConfigProvider, MockCurrencyProvider } from '../../utils/tests';
import { MockConfigProvider, MockCurrencyProvider } from '../../testUtils';
import { defaultCurrencies } from '../../utils/currency';
const engineers = findAlways(defaultCurrencies, c => c.kind === null);
@@ -19,7 +19,7 @@ import { default as HappyFace } from '@material-ui/icons/SentimentSatisfiedAlt';
import { renderInTestApp } from '@backstage/test-utils';
import { CostInsightsNavigation } from './CostInsightsNavigation';
import { Product, Icon } from '../../types';
import { MockConfigProvider, MockScrollProvider } from '../../utils/tests';
import { MockConfigProvider, MockScrollProvider } from '../../testUtils';
import { getDefaultNavigationItems } from '../../utils/navigation';
const mockIcons: Icon[] = [
@@ -18,7 +18,7 @@ import React from 'react';
import { CostInsightsTabs } from './CostInsightsTabs';
import UserEvent from '@testing-library/user-event';
import { Group } from '../../types';
import { MockFilterProvider, MockLoadingProvider } from '../../utils/tests';
import { MockFilterProvider, MockLoadingProvider } from '../../testUtils';
import { renderInTestApp } from '@backstage/test-utils';
const mockSetPageFilters = jest.fn();
@@ -24,13 +24,11 @@ import {
getGroupedProjects,
MockAggregatedDailyCosts,
trendlineOf,
} from '../../utils/mockData';
import {
MockBillingDateProvider,
MockConfigProvider,
MockFilterProvider,
MockScrollProvider,
} from '../../utils/tests';
} from '../../testUtils';
import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider';
const mockGroupDailyCost: Cost = {
@@ -20,7 +20,7 @@ import { renderInTestApp } from '@backstage/test-utils';
import UserEvent from '@testing-library/user-event';
import { PeriodSelect, getDefaultOptions } from './PeriodSelect';
import { getDefaultPageFilters } from '../../utils/filters';
import { MockBillingDateProvider } from '../../utils/tests';
import { MockBillingDateProvider } from '../../testUtils';
import { Group, Duration } from '../../types';
const DefaultPageFilters = getDefaultPageFilters([{ id: 'tools' }] as Group[]);
@@ -18,8 +18,8 @@ import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ProductInsights } from './ProductInsights';
import { ProductInsightsOptions } from '../../api';
import { mockDefaultLoadingState } from '../../utils/mockData';
import {
mockDefaultLoadingState,
MockConfigProvider,
MockCostInsightsApiProvider,
MockCurrencyProvider,
@@ -27,7 +27,7 @@ import {
MockBillingDateProvider,
MockScrollProvider,
MockLoadingProvider,
} from '../../utils/tests';
} from '../../testUtils';
import { Entity, Product } from '../../types';
// suppress recharts componentDidUpdate warnings
@@ -22,15 +22,13 @@ import {
createMockEntity,
mockDefaultLoadingState,
MockComputeEngine,
} from '../../utils/mockData';
import {
MockConfigProvider,
MockCostInsightsApiProvider,
MockCurrencyProvider,
MockBillingDateProvider,
MockScrollProvider,
MockLoadingProvider,
} from '../../utils/tests';
} from '../../testUtils';
import { Duration, Entity, Product } from '../../types';
// suppress recharts componentDidUpdate warnings
@@ -17,12 +17,12 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ProjectGrowthAlertCard } from './ProjectGrowthAlertCard';
import { createMockProjectGrowthData } from '../../utils/mockData';
import {
createMockProjectGrowthData,
MockCurrencyProvider,
MockConfigProvider,
MockBillingDateProvider,
} from '../../utils/tests';
} from '../../testUtils';
import { AlertCost } from '../../types';
import { defaultCurrencies } from '../../utils/currency';
import { findAlways } from '../../utils/assert';
@@ -18,7 +18,7 @@ import React from 'react';
import { getByRole, waitFor } from '@testing-library/react';
import UserEvent from '@testing-library/user-event';
import { ProjectSelect } from './ProjectSelect';
import { MockFilterProvider } from '../../utils/tests';
import { MockFilterProvider } from '../../testUtils';
import { renderInTestApp } from '@backstage/test-utils';
const mockProjects = [
@@ -19,7 +19,7 @@ import { UnlabeledDataflowAlertCard } from './UnlabeledDataflowAlertCard';
import {
createMockUnlabeledDataflowData,
createMockUnlabeledDataflowAlertProject,
} from '../../utils/mockData';
} from '../../testUtils';
import { renderInTestApp } from '@backstage/test-utils';
const MockUnlabeledDataflowAlertMultipleProjects = createMockUnlabeledDataflowData(
@@ -14,6 +14,28 @@
* limitations under the License.
*/
export * from './ActionItems';
export * from './AlertInsights';
export * from './AlertInstructionsLayout';
export * from './BarChart';
export * from './CopyUrlToClipboard';
export * from './CostGrowth';
export * from './CostInsightsHeader';
export * from './CostInsightsLayout';
export * from './CostInsightsNavigation';
export * from './CostInsightsPage';
export * from './CostInsightsSupportButton';
export * from './CostInsightsTabs';
export * from './CostOverviewCard';
export * from './CurrencySelect';
export * from './LabelDataflowInstructionsPage';
export * from './LegendItem';
export * from './MetricSelect';
export * from './PeriodSelect';
export * from './ProductInsights';
export * from './ProductInsightsCard';
export * from './ProjectGrowthAlertCard';
export * from './ProjectGrowthInstructionsPage';
export * from './ProjectSelect';
export * from './UnlabeledDataflowAlertCard';
export * from './WhyCostsMatter';
@@ -16,8 +16,8 @@
import React from 'react';
import pluralize from 'pluralize';
import { MigrationAlertCard } from '../components/MigrationAlertCard';
import { CostInsightsApi } from '../api';
import { KubernetesMigrationAlertCard } from '../components';
import { CostInsightsApi } from '../../api';
import {
Alert,
AlertForm,
@@ -26,19 +26,22 @@ import {
AlertSnoozeFormData,
ChangeStatistic,
Entity,
} from '../types';
import { MigrationDismissForm, MigrationDismissFormData } from '../forms';
} from '../../types';
import {
KubernetesMigrationDismissForm,
KubernetesMigrationDismissFormData,
} from '../forms';
export interface MigrationData {
export interface KubernetesMigrationData {
startDate: string;
endDate: string;
change: ChangeStatistic;
services: Array<Entity>;
}
export interface MigrationAlert extends Alert {
export interface KubernetesMigrationApi extends Alert {
api: CostInsightsApi;
data: MigrationData;
data: KubernetesMigrationData;
}
/**
@@ -63,9 +66,9 @@ export interface MigrationAlert extends Alert {
* Custom forms must implement the corresponding event hook. See /forms for example implementations.
*/
export class KubernetesMigrationAlert implements MigrationAlert {
export class KubernetesMigrationAlert implements KubernetesMigrationApi {
api: CostInsightsApi;
data: MigrationData;
data: KubernetesMigrationData;
subtitle =
'Services running on Kubernetes are estimated to save 50% or more compared to Compute Engine.';
@@ -74,11 +77,11 @@ export class KubernetesMigrationAlert implements MigrationAlert {
AcceptForm = null;
// Overrides default Dismiss form with a custom form component.
DismissForm: AlertForm<
MigrationAlert,
MigrationDismissFormData
> = MigrationDismissForm;
KubernetesMigrationAlert,
KubernetesMigrationDismissFormData
> = KubernetesMigrationDismissForm;
constructor(api: CostInsightsApi, data: MigrationData) {
constructor(api: CostInsightsApi, data: KubernetesMigrationData) {
this.api = api;
this.data = data;
}
@@ -98,7 +101,7 @@ export class KubernetesMigrationAlert implements MigrationAlert {
true,
)}, sorted by cost`;
return (
<MigrationAlertCard
<KubernetesMigrationAlertCard
data={this.data}
title="Migrate to Kubernetes"
subheader={subheader}
@@ -110,7 +113,7 @@ export class KubernetesMigrationAlert implements MigrationAlert {
/* Fires when the onSubmit event is raised on a Dismiss form. Displays custom dismiss form. */
async onDismissed(
options: AlertOptions<MigrationDismissFormData>,
options: AlertOptions<KubernetesMigrationDismissFormData>,
): Promise<Alert[]> {
const alerts = await this.api.getAlerts(options.group);
return new Promise(resolve =>
@@ -0,0 +1,19 @@
/*
* 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 { KubernetesMigrationAlert } from './KubernetesMigrationAlert';
export type { KubernetesMigrationApi } from './KubernetesMigrationAlert';
export type { KubernetesMigrationData } from './KubernetesMigrationAlert';
@@ -16,7 +16,7 @@
/* eslint-disable no-restricted-imports */
import dayjs from 'dayjs';
import { CostInsightsApi, ProductInsightsOptions } from '../src/api';
import { CostInsightsApi, ProductInsightsOptions } from '../api';
import {
Alert,
Cost,
@@ -27,12 +27,9 @@ import {
Project,
ProjectGrowthData,
UnlabeledDataflowData,
} from '../src/types';
import {
ProjectGrowthAlert,
UnlabeledDataflowAlert,
KubernetesMigrationAlert,
} from '../src/alerts';
} from '../types';
import { KubernetesMigrationAlert } from './alerts';
import { ProjectGrowthAlert, UnlabeledDataflowAlert } from '../alerts';
import {
aggregationFor,
changeOf,
@@ -40,7 +37,7 @@ import {
getGroupedProducts,
getGroupedProjects,
trendlineOf,
} from './utils/mockData';
} from '../testUtils';
export class ExampleCostInsightsClient implements CostInsightsApi {
private request(_: any, res: any): Promise<any> {
@@ -17,30 +17,30 @@
import React from 'react';
import { Box } from '@material-ui/core';
import { InfoCard } from '@backstage/core';
import { MigrationBarChartLegend } from './MigrationBarChartLegend';
import { MigrationBarChart } from './MigrationBarChart';
import { MigrationData } from '../../alerts';
import { KubernetesMigrationBarChartLegend } from './KubernetesMigrationBarChartLegend';
import { KubernetesMigrationBarChart } from './KubernetesMigrationBarChart';
import { KubernetesMigrationData } from '../../alerts';
type MigrationAlertProps = {
data: MigrationData;
type KubernetesMigrationAlertProps = {
data: KubernetesMigrationData;
title: string;
subheader: string;
currentProduct: string;
comparedProduct: string;
};
export const MigrationAlertCard = ({
export const KubernetesMigrationAlertCard = ({
data,
title,
subheader,
currentProduct,
comparedProduct,
}: MigrationAlertProps) => {
}: KubernetesMigrationAlertProps) => {
return (
<InfoCard title={title} subheader={subheader}>
<Box display="flex" flexDirection="column">
<Box paddingY={1}>
<MigrationBarChartLegend
<KubernetesMigrationBarChartLegend
startDate={data.startDate}
change={data.change}
currentProduct={currentProduct}
@@ -48,7 +48,7 @@ export const MigrationAlertCard = ({
/>
</Box>
<Box paddingY={1}>
<MigrationBarChart
<KubernetesMigrationBarChart
services={data.services}
currentProduct={currentProduct}
comparedProduct={comparedProduct}
@@ -15,13 +15,13 @@
*/
import React from 'react';
import { BarChart } from '../BarChart';
import { BarChart } from '../../../components';
import {
BarChartOptions,
CostInsightsTheme,
Entity,
ResourceData,
} from '../../types';
} from '../../../types';
import { useTheme } from '@material-ui/core';
type MigrationBarChartProps = {
@@ -30,7 +30,7 @@ type MigrationBarChartProps = {
services: Array<Entity>;
};
export const MigrationBarChart = ({
export const KubernetesMigrationBarChart = ({
currentProduct,
comparedProduct,
services,
@@ -16,10 +16,9 @@
import React from 'react';
import { Box, useTheme } from '@material-ui/core';
import { CostGrowth } from '../CostGrowth';
import { LegendItem } from '../LegendItem';
import { ChangeStatistic, CostInsightsTheme, Duration } from '../../types';
import { monthOf } from '../../utils/formatters';
import { CostGrowth, LegendItem } from '../../../components';
import { ChangeStatistic, CostInsightsTheme, Duration } from '../../../types';
import { monthOf } from '../../../utils/formatters';
export type MigrationBarChartLegendProps = {
change: ChangeStatistic;
@@ -28,7 +27,7 @@ export type MigrationBarChartLegendProps = {
comparedProduct: string;
};
export const MigrationBarChartLegend = ({
export const KubernetesMigrationBarChartLegend = ({
currentProduct,
comparedProduct,
change,
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { MigrationAlertCard } from './MigrationAlertCard';
export * from './KubernetesMigrationAlertCard';
@@ -0,0 +1,17 @@
/*
* 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 * from './KubernetesMigrationAlertCard';
@@ -28,22 +28,22 @@ import {
FormGroup,
Typography,
} from '@material-ui/core';
import { AlertFormProps, Entity } from '../types';
import { MigrationAlert } from '../alerts';
import { findAlways } from '../utils/assert';
import { AlertFormProps, Entity } from '../../types';
import { KubernetesMigrationAlert } from '../alerts';
import { findAlways } from '../../utils/assert';
export type MigrationDismissFormData = {
export type KubernetesMigrationDismissFormData = {
services: Entity[];
};
export type MigrationDismissFormProps = AlertFormProps<
MigrationAlert,
MigrationDismissFormData
export type KubernetesMigrationDismissFormProps = AlertFormProps<
KubernetesMigrationAlert,
KubernetesMigrationDismissFormData
>;
export const MigrationDismissForm = forwardRef<
export const KubernetesMigrationDismissForm = forwardRef<
HTMLFormElement,
MigrationDismissFormProps
KubernetesMigrationDismissFormProps
>(({ onSubmit, disableSubmit, alert }, ref) => {
const [services, setServices] = useState<Entity[]>(alert.data.services);
@@ -0,0 +1,17 @@
/*
* 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 * from './KubernetesMigrationDismissForm';
@@ -0,0 +1,20 @@
/*
* 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 * from './alerts';
export * from './components';
export * from './forms';
export * from './client';
-2
View File
@@ -17,5 +17,3 @@
export { AlertAcceptForm } from './AlertAcceptForm';
export { AlertSnoozeForm } from './AlertSnoozeForm';
export { AlertDismissForm } from './AlertDismissForm';
export { MigrationDismissForm } from './MigrationDismissForm';
export type { MigrationDismissFormData } from './MigrationDismissForm';
+4 -5
View File
@@ -21,10 +21,9 @@ export {
CostInsightsProjectGrowthInstructionsPage,
CostInsightsLabelDataflowInstructionsPage,
} from './plugin';
export * from './client';
export { ExampleCostInsightsClient } from './example';
export { BarChart, LegendItem, CostGrowth } from './components';
export { MockConfigProvider, MockCurrencyProvider } from './testUtils';
export * from './api';
export { ProjectGrowthAlert, UnlabeledDataflowAlert } from './alerts';
export * from './components';
export { useCurrency } from './hooks';
export * from './alerts';
export * from './types';
export * from './utils/tests';
@@ -0,0 +1,78 @@
/*
* 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 {
ProjectGrowthData,
UnlabeledDataflowAlertProject,
UnlabeledDataflowData,
} from '../types';
type mockAlertRenderer<T> = (alert: T) => T;
type mockEntityRenderer<T> = (entity: T) => T;
export const createMockUnlabeledDataflowData = (
callback?: mockAlertRenderer<UnlabeledDataflowData>,
): UnlabeledDataflowData => {
const data: UnlabeledDataflowData = {
periodStart: '2020-05-01',
periodEnd: '2020-06-1',
projects: [],
unlabeledCost: 0,
labeledCost: 0,
};
if (typeof callback === 'function') {
return callback({ ...data });
}
return { ...data };
};
export const createMockUnlabeledDataflowAlertProject = (
callback?: mockEntityRenderer<UnlabeledDataflowAlertProject>,
): UnlabeledDataflowAlertProject => {
const defaultProject: UnlabeledDataflowAlertProject = {
id: 'test-alert-project',
unlabeledCost: 2000.0,
labeledCost: 3200.0,
};
if (typeof callback === 'function') {
return callback({ ...defaultProject });
}
return { ...defaultProject };
};
export const createMockProjectGrowthData = (
callback?: mockAlertRenderer<ProjectGrowthData>,
): ProjectGrowthData => {
const data: ProjectGrowthData = {
project: 'test-project-growth-alert',
periodStart: '2019-Q4',
periodEnd: '2020-Q1',
aggregation: [670532.1, 970502.8],
change: {
ratio: 0.5,
amount: 180000,
},
products: [],
};
if (typeof callback === 'function') {
return callback({ ...data });
}
return { ...data };
};
@@ -0,0 +1,33 @@
/*
* 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 { Config } from '@backstage/config';
import { ConfigApi } from '@backstage/core';
import { MockProductTypes } from './products';
export const MockProductsConfig: Partial<ConfigApi> = {
keys: () => Object.keys(MockProductTypes),
};
export const MockMetricsConfig: Partial<ConfigApi> = {
getOptionalString: () => 'daily-cost',
keys: () => ['daily-cost'],
};
export const MockCostInsightsConfig: Partial<Config> = {
getConfig: () => MockProductsConfig as Config,
getOptionalConfig: () => MockMetricsConfig as Config,
};
@@ -0,0 +1,22 @@
/*
* 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, ProductFilters } from '../types';
import { MockProductTypes } from './products';
export const MockProductFilters: ProductFilters = Object.keys(
MockProductTypes,
).map(productType => ({ duration: Duration.P30D, productType }));
@@ -0,0 +1,23 @@
/*
* 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 * from './alerts';
export * from './config';
export * from './filters';
export * from './loading';
export * from './mockData';
export * from './products';
export * from './providers';
export * from './testUtils';
@@ -0,0 +1,30 @@
/*
* 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 { MockProducts } from './products';
import {
DefaultLoadingAction,
getDefaultState as getDefaultLoadingState,
} from '../utils/loading';
export const MockDefaultLoadingActions = ([
DefaultLoadingAction.UserGroups,
DefaultLoadingAction.CostInsightsInitial,
DefaultLoadingAction.CostInsightsPage,
] as string[]).concat(MockProducts.map(product => product.kind));
export const mockDefaultLoadingState = getDefaultLoadingState(
MockDefaultLoadingActions,
);
@@ -14,264 +14,7 @@
* limitations under the License.
*/
import dayjs from 'dayjs';
import regression, { DataPoint } from 'regression';
import { Config } from '@backstage/config';
import { ConfigApi } from '@backstage/core';
import {
ChangeStatistic,
Duration,
Entity,
Product,
ProductFilters,
ProjectGrowthData,
Trendline,
UnlabeledDataflowAlertProject,
UnlabeledDataflowData,
DateAggregation,
DEFAULT_DATE_FORMAT,
} from '../types';
import {
DefaultLoadingAction,
getDefaultState as getDefaultLoadingState,
} from '../utils/loading';
import { findAlways } from '../utils/assert';
import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration';
type mockAlertRenderer<T> = (alert: T) => T;
type mockEntityRenderer<T> = (entity: T) => T;
type IntervalFields = {
duration: Duration;
endDate: string;
};
export const createMockEntity = (
callback?: mockEntityRenderer<Entity>,
): Entity => {
const defaultEntity: Entity = {
id: 'test-entity',
aggregation: [100, 200],
entities: {},
change: {
ratio: 0,
amount: 0,
},
};
if (typeof callback === 'function') {
return callback({ ...defaultEntity });
}
return { ...defaultEntity };
};
export const createMockProduct = (
callback?: mockEntityRenderer<Product>,
): Product => {
const defaultProduct: Product = {
kind: 'compute-engine',
name: 'Compute Engine',
};
if (typeof callback === 'function') {
return callback({ ...defaultProduct });
}
return { ...defaultProduct };
};
export const createMockProjectGrowthData = (
callback?: mockAlertRenderer<ProjectGrowthData>,
): ProjectGrowthData => {
const data: ProjectGrowthData = {
project: 'test-project-growth-alert',
periodStart: '2019-Q4',
periodEnd: '2020-Q1',
aggregation: [670532.1, 970502.8],
change: {
ratio: 0.5,
amount: 180000,
},
products: [],
};
if (typeof callback === 'function') {
return callback({ ...data });
}
return { ...data };
};
export const createMockUnlabeledDataflowData = (
callback?: mockAlertRenderer<UnlabeledDataflowData>,
): UnlabeledDataflowData => {
const data: UnlabeledDataflowData = {
periodStart: '2020-05-01',
periodEnd: '2020-06-1',
projects: [],
unlabeledCost: 0,
labeledCost: 0,
};
if (typeof callback === 'function') {
return callback({ ...data });
}
return { ...data };
};
export const createMockUnlabeledDataflowAlertProject = (
callback?: mockEntityRenderer<UnlabeledDataflowAlertProject>,
): UnlabeledDataflowAlertProject => {
const defaultProject: UnlabeledDataflowAlertProject = {
id: 'test-alert-project',
unlabeledCost: 2000.0,
labeledCost: 3200.0,
};
if (typeof callback === 'function') {
return callback({ ...defaultProject });
}
return { ...defaultProject };
};
export const MockProductTypes: Record<string, string> = {
'compute-engine': 'Compute Engine',
'cloud-dataflow': 'Cloud Dataflow',
'cloud-storage': 'Cloud Storage',
'big-query': 'Big Query',
'big-table': 'BigTable',
'cloud-pub-sub': 'Cloud Pub/Sub',
};
export const MockProductFilters: ProductFilters = Object.keys(
MockProductTypes,
).map(productType => ({ duration: Duration.P30D, productType }));
export const MockProducts: Product[] = Object.keys(MockProductTypes).map(
productType =>
createMockProduct(() => ({
kind: productType,
name: MockProductTypes[productType],
})),
);
export const MockDefaultLoadingActions = ([
DefaultLoadingAction.UserGroups,
DefaultLoadingAction.CostInsightsInitial,
DefaultLoadingAction.CostInsightsPage,
] as string[]).concat(MockProducts.map(product => product.kind));
export const mockDefaultLoadingState = getDefaultLoadingState(
MockDefaultLoadingActions,
);
export const MockComputeEngine = findAlways(
MockProducts,
p => p.kind === 'compute-engine',
);
export const MockCloudDataflow = findAlways(
MockProducts,
p => p.kind === 'cloud-dataflow',
);
export const MockCloudStorage = findAlways(
MockProducts,
p => p.kind === 'cloud-storage',
);
export const MockBigQuery = findAlways(
MockProducts,
p => p.kind === 'big-query',
);
export const MockBigtable = findAlways(
MockProducts,
p => p.kind === 'big-table',
);
export const MockProductsConfig: Partial<ConfigApi> = {
keys: () => Object.keys(MockProductTypes),
};
export const MockMetricsConfig: Partial<ConfigApi> = {
getOptionalString: () => 'daily-cost',
keys: () => ['daily-cost'],
};
export const MockCostInsightsConfig: Partial<Config> = {
getConfig: () => MockProductsConfig as Config,
getOptionalConfig: () => MockMetricsConfig as Config,
};
export function trendlineOf(aggregation: DateAggregation[]): Trendline {
const data: ReadonlyArray<DataPoint> = aggregation.map(a => [
Date.parse(a.date) / 1000,
a.amount,
]);
const result = regression.linear(data, { precision: 5 });
return {
slope: result.equation[0],
intercept: result.equation[1],
};
}
export function changeOf(aggregation: DateAggregation[]): ChangeStatistic {
const firstAmount = aggregation.length ? aggregation[0].amount : 0;
const lastAmount = aggregation.length
? aggregation[aggregation.length - 1].amount
: 0;
const ratio =
firstAmount !== 0 ? (lastAmount - firstAmount) / firstAmount : 0;
return {
ratio: ratio,
amount: lastAmount - firstAmount,
};
}
export function aggregationFor(
intervals: string,
baseline: number,
): DateAggregation[] {
const { duration, endDate } = parseIntervals(intervals);
const inclusiveEndDate = inclusiveEndDateOf(duration, endDate);
const days = dayjs(endDate).diff(
inclusiveStartDateOf(duration, inclusiveEndDate),
'day',
);
function nextDelta(): number {
const varianceFromBaseline = 0.15;
// Let's give positive vibes in trendlines - higher change for positive delta with >0.5 value
const positiveTrendChance = 0.55;
const normalization = positiveTrendChance - 1;
return baseline * (Math.random() + normalization) * varianceFromBaseline;
}
return [...Array(days).keys()].reduce(
(values: DateAggregation[], i: number): DateAggregation[] => {
const last = values.length ? values[values.length - 1].amount : baseline;
const date = dayjs(inclusiveStartDateOf(duration, inclusiveEndDate))
.add(i, 'day')
.format(DEFAULT_DATE_FORMAT);
const amount = Math.max(0, last + nextDelta());
values.push({
date: date,
amount: amount,
});
return values;
},
[],
);
}
function parseIntervals(intervals: string): IntervalFields {
const match = intervals.match(
/\/(?<duration>P\d+[DM])\/(?<date>\d{4}-\d{2}-\d{2})/,
);
if (Object.keys(match?.groups || {}).length !== 2) {
throw new Error(`Invalid intervals: ${intervals}`);
}
const { duration, date } = match!.groups!;
return {
duration: duration as Duration,
endDate: date,
};
}
import { DateAggregation, Entity } from '../types';
export const MockAggregatedDailyCosts: DateAggregation[] = [
{
@@ -520,7 +263,7 @@ export const MockAggregatedDailyCosts: DateAggregation[] = [
},
];
export const SampleBigQueryInsights: Entity = {
export const MockBigQueryInsights: Entity = {
id: 'bigQuery',
aggregation: [10_000, 30_000],
change: {
@@ -560,7 +303,7 @@ export const SampleBigQueryInsights: Entity = {
},
};
export const SampleCloudDataflowInsights: Entity = {
export const MockCloudDataflowInsights: Entity = {
id: 'cloudDataflow',
aggregation: [100_000, 158_000],
change: {
@@ -579,7 +322,7 @@ export const SampleCloudDataflowInsights: Entity = {
entities: {
SKU: [
{
id: 'Sample SKU A',
id: 'Mock SKU A',
aggregation: [3_000, 4_000],
change: {
ratio: 0.333333,
@@ -588,7 +331,7 @@ export const SampleCloudDataflowInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU B',
id: 'Mock SKU B',
aggregation: [7_000, 8_000],
change: {
ratio: 0.14285714,
@@ -609,7 +352,7 @@ export const SampleCloudDataflowInsights: Entity = {
entities: {
SKU: [
{
id: 'Sample SKU A',
id: 'Mock SKU A',
aggregation: [20_000, 15_000],
change: {
ratio: -0.25,
@@ -618,7 +361,7 @@ export const SampleCloudDataflowInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU B',
id: 'Mock SKU B',
aggregation: [30_000, 35_000],
change: {
ratio: -0.16666666666666666,
@@ -627,7 +370,7 @@ export const SampleCloudDataflowInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU C',
id: 'Mock SKU C',
aggregation: [10_000, 20_000],
change: {
ratio: 1,
@@ -648,7 +391,7 @@ export const SampleCloudDataflowInsights: Entity = {
entities: {
SKU: [
{
id: 'Sample SKU A',
id: 'Mock SKU A',
aggregation: [4_000, 4_000],
change: {
ratio: 0,
@@ -657,7 +400,7 @@ export const SampleCloudDataflowInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU B',
id: 'Mock SKU B',
aggregation: [8_000, 4_000],
change: {
ratio: -0.5,
@@ -681,7 +424,7 @@ export const SampleCloudDataflowInsights: Entity = {
},
};
export const SampleCloudStorageInsights: Entity = {
export const MockCloudStorageInsights: Entity = {
id: 'cloudStorage',
aggregation: [45_000, 45_000],
change: {
@@ -700,7 +443,7 @@ export const SampleCloudStorageInsights: Entity = {
entities: {
SKU: [
{
id: 'Sample SKU A',
id: 'Mock SKU A',
aggregation: [10_000, 11_000],
change: {
ratio: 0.1,
@@ -709,7 +452,7 @@ export const SampleCloudStorageInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU B',
id: 'Mock SKU B',
aggregation: [2_000, 5_000],
change: {
ratio: 1.5,
@@ -718,7 +461,7 @@ export const SampleCloudStorageInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU C',
id: 'Mock SKU C',
aggregation: [3_000, 4_000],
change: {
ratio: 0.3333,
@@ -739,7 +482,7 @@ export const SampleCloudStorageInsights: Entity = {
entities: {
SKU: [
{
id: 'Sample SKU A',
id: 'Mock SKU A',
aggregation: [12_000, 13_000],
change: {
ratio: 0.08333333333333333,
@@ -748,7 +491,7 @@ export const SampleCloudStorageInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU B',
id: 'Mock SKU B',
aggregation: [16_000, 12_000],
change: {
ratio: -0.25,
@@ -757,7 +500,7 @@ export const SampleCloudStorageInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU C',
id: 'Mock SKU C',
aggregation: [2_000, 0],
change: {
ratio: -1,
@@ -781,7 +524,7 @@ export const SampleCloudStorageInsights: Entity = {
},
};
export const SampleComputeEngineInsights: Entity = {
export const MockComputeEngineInsights: Entity = {
id: 'computeEngine',
aggregation: [80_000, 90_000],
change: {
@@ -800,7 +543,7 @@ export const SampleComputeEngineInsights: Entity = {
entities: {
SKU: [
{
id: 'Sample SKU A',
id: 'Mock SKU A',
aggregation: [4_000, 2_000],
change: {
ratio: -0.5,
@@ -809,7 +552,7 @@ export const SampleComputeEngineInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU B',
id: 'Mock SKU B',
aggregation: [7_000, 6_000],
change: {
ratio: -0.14285714285714285,
@@ -818,7 +561,7 @@ export const SampleComputeEngineInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU C',
id: 'Mock SKU C',
aggregation: [9_000, 2_000],
change: {
ratio: -0.7777777777777778,
@@ -859,7 +602,7 @@ export const SampleComputeEngineInsights: Entity = {
entities: {
SKU: [
{
id: 'Sample SKU A',
id: 'Mock SKU A',
aggregation: [1_000, 2_000],
change: {
ratio: 1,
@@ -868,7 +611,7 @@ export const SampleComputeEngineInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU B',
id: 'Mock SKU B',
aggregation: [4_000, 8_000],
change: {
ratio: 1,
@@ -877,7 +620,7 @@ export const SampleComputeEngineInsights: Entity = {
entities: {},
},
{
id: 'Sample SKU C',
id: 'Mock SKU C',
aggregation: [5_000, 10_000],
change: {
ratio: 1,
@@ -921,7 +664,7 @@ export const SampleComputeEngineInsights: Entity = {
},
};
export const SampleEventsInsights: Entity = {
export const MockEventsInsights: Entity = {
id: 'events',
aggregation: [20_000, 10_000],
change: {
@@ -940,7 +683,7 @@ export const SampleEventsInsights: Entity = {
entities: {
product: [
{
id: 'Sample Product A',
id: 'Mock Product A',
aggregation: [5_000, 2_000],
change: {
ratio: -0.6,
@@ -949,7 +692,7 @@ export const SampleEventsInsights: Entity = {
entities: {},
},
{
id: 'Sample Product B',
id: 'Mock Product B',
aggregation: [7_000, 2_500],
change: {
ratio: -0.64285714285,
@@ -958,7 +701,7 @@ export const SampleEventsInsights: Entity = {
entities: {},
},
{
id: 'Sample Product C',
id: 'Mock Product C',
aggregation: [3_000, 2_500],
change: {
ratio: -0.16666666666,
@@ -979,7 +722,7 @@ export const SampleEventsInsights: Entity = {
entities: {
product: [
{
id: 'Sample Product A',
id: 'Mock Product A',
aggregation: [2_000, 1_000],
change: {
ratio: -0.5,
@@ -988,7 +731,7 @@ export const SampleEventsInsights: Entity = {
entities: {},
},
{
id: 'Sample Product B',
id: 'Mock Product B',
aggregation: [1_000, 1_500],
change: {
ratio: 0.5,
@@ -997,7 +740,7 @@ export const SampleEventsInsights: Entity = {
entities: {},
},
{
id: 'Sample Product C',
id: 'Mock Product C',
aggregation: [2_000, 500],
change: {
ratio: -0.75,
@@ -1011,72 +754,3 @@ export const SampleEventsInsights: Entity = {
],
},
};
export function entityOf(product: string): Entity {
switch (product) {
case 'computeEngine':
return SampleComputeEngineInsights;
case 'cloudDataflow':
return SampleCloudDataflowInsights;
case 'cloudStorage':
return SampleCloudStorageInsights;
case 'bigQuery':
return SampleBigQueryInsights;
case 'events':
return SampleEventsInsights;
default:
throw new Error(
`Cannot get insights for ${product}. Make sure product matches product property in app-info.yaml`,
);
}
}
export const getGroupedProducts = (intervals: string) => [
{
id: 'Cloud Dataflow',
aggregation: aggregationFor(intervals, 1_700),
},
{
id: 'Compute Engine',
aggregation: aggregationFor(intervals, 350),
},
{
id: 'Cloud Storage',
aggregation: aggregationFor(intervals, 1_300),
},
{
id: 'BigQuery',
aggregation: aggregationFor(intervals, 2_000),
},
{
id: 'Cloud SQL',
aggregation: aggregationFor(intervals, 750),
},
{
id: 'Cloud Spanner',
aggregation: aggregationFor(intervals, 50),
},
{
id: 'Cloud Pub/Sub',
aggregation: aggregationFor(intervals, 1_000),
},
{
id: 'Cloud Bigtable',
aggregation: aggregationFor(intervals, 250),
},
];
export const getGroupedProjects = (intervals: string) => [
{
id: 'project-a',
aggregation: aggregationFor(intervals, 1_700),
},
{
id: 'project-b',
aggregation: aggregationFor(intervals, 350),
},
{
id: 'project-c',
aggregation: aggregationFor(intervals, 1_300),
},
];
@@ -0,0 +1,90 @@
/*
* 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 { Entity, Product } from '../types';
import { findAlways } from '../utils/assert';
type mockEntityRenderer<T> = (entity: T) => T;
export const createMockEntity = (
callback?: mockEntityRenderer<Entity>,
): Entity => {
const defaultEntity: Entity = {
id: 'test-entity',
aggregation: [100, 200],
entities: {},
change: {
ratio: 0,
amount: 0,
},
};
if (typeof callback === 'function') {
return callback({ ...defaultEntity });
}
return { ...defaultEntity };
};
export const createMockProduct = (
callback?: mockEntityRenderer<Product>,
): Product => {
const defaultProduct: Product = {
kind: 'compute-engine',
name: 'Compute Engine',
};
if (typeof callback === 'function') {
return callback({ ...defaultProduct });
}
return { ...defaultProduct };
};
export const MockProductTypes: Record<string, string> = {
'compute-engine': 'Compute Engine',
'cloud-dataflow': 'Cloud Dataflow',
'cloud-storage': 'Cloud Storage',
'big-query': 'Big Query',
'big-table': 'BigTable',
'cloud-pub-sub': 'Cloud Pub/Sub',
};
export const MockProducts: Product[] = Object.keys(MockProductTypes).map(
productType =>
createMockProduct(() => ({
kind: productType,
name: MockProductTypes[productType],
})),
);
export const MockComputeEngine = findAlways(
MockProducts,
p => p.kind === 'compute-engine',
);
export const MockCloudDataflow = findAlways(
MockProducts,
p => p.kind === 'cloud-dataflow',
);
export const MockCloudStorage = findAlways(
MockProducts,
p => p.kind === 'cloud-storage',
);
export const MockBigQuery = findAlways(
MockProducts,
p => p.kind === 'big-query',
);
export const MockBigtable = findAlways(
MockProducts,
p => p.kind === 'big-table',
);
@@ -34,18 +34,6 @@ import {
import { ScrollContext, ScrollContextProps } from '../hooks/useScroll';
import { Group, Duration } from '../types';
/*
Mock Providers and types are exposed publicly to allow users to test custom implementations
such as alerts, which may require context.
Utility functions such as getDefaultPageFilters, etc. are intentionally
omitted as we do not want to expose explictly or implicitly internal implementations
that may be subject to change.
Each Mock Provider provides minimal defaults which can be overridden, allowing users to define
context props only when necessary.
*/
type PartialPropsWithChildren<T> = PropsWithChildren<Partial<T>>;
export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }];
@@ -0,0 +1,183 @@
/*
* 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 dayjs from 'dayjs';
import regression, { DataPoint } from 'regression';
import {
ChangeStatistic,
Duration,
Entity,
Trendline,
DateAggregation,
DEFAULT_DATE_FORMAT,
} from '../types';
import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration';
import {
MockComputeEngineInsights,
MockCloudDataflowInsights,
MockCloudStorageInsights,
MockBigQueryInsights,
MockEventsInsights,
} from './mockData';
type IntervalFields = {
duration: Duration;
endDate: string;
};
function parseIntervals(intervals: string): IntervalFields {
const match = intervals.match(
/\/(?<duration>P\d+[DM])\/(?<date>\d{4}-\d{2}-\d{2})/,
);
if (Object.keys(match?.groups || {}).length !== 2) {
throw new Error(`Invalid intervals: ${intervals}`);
}
const { duration, date } = match!.groups!;
return {
duration: duration as Duration,
endDate: date,
};
}
export function aggregationFor(
intervals: string,
baseline: number,
): DateAggregation[] {
const { duration, endDate } = parseIntervals(intervals);
const inclusiveEndDate = inclusiveEndDateOf(duration, endDate);
const days = dayjs(endDate).diff(
inclusiveStartDateOf(duration, inclusiveEndDate),
'day',
);
function nextDelta(): number {
const varianceFromBaseline = 0.15;
// Let's give positive vibes in trendlines - higher change for positive delta with >0.5 value
const positiveTrendChance = 0.55;
const normalization = positiveTrendChance - 1;
return baseline * (Math.random() + normalization) * varianceFromBaseline;
}
return [...Array(days).keys()].reduce(
(values: DateAggregation[], i: number): DateAggregation[] => {
const last = values.length ? values[values.length - 1].amount : baseline;
const date = dayjs(inclusiveStartDateOf(duration, inclusiveEndDate))
.add(i, 'day')
.format(DEFAULT_DATE_FORMAT);
const amount = Math.max(0, last + nextDelta());
values.push({
date: date,
amount: amount,
});
return values;
},
[],
);
}
export function changeOf(aggregation: DateAggregation[]): ChangeStatistic {
const firstAmount = aggregation.length ? aggregation[0].amount : 0;
const lastAmount = aggregation.length
? aggregation[aggregation.length - 1].amount
: 0;
const ratio =
firstAmount !== 0 ? (lastAmount - firstAmount) / firstAmount : 0;
return {
ratio: ratio,
amount: lastAmount - firstAmount,
};
}
export function trendlineOf(aggregation: DateAggregation[]): Trendline {
const data: ReadonlyArray<DataPoint> = aggregation.map(a => [
Date.parse(a.date) / 1000,
a.amount,
]);
const result = regression.linear(data, { precision: 5 });
return {
slope: result.equation[0],
intercept: result.equation[1],
};
}
export function entityOf(product: string): Entity {
switch (product) {
case 'computeEngine':
return MockComputeEngineInsights;
case 'cloudDataflow':
return MockCloudDataflowInsights;
case 'cloudStorage':
return MockCloudStorageInsights;
case 'bigQuery':
return MockBigQueryInsights;
case 'events':
return MockEventsInsights;
default:
throw new Error(
`Cannot get insights for ${product}. Make sure product matches product property in app-info.yaml`,
);
}
}
export const getGroupedProducts = (intervals: string) => [
{
id: 'Cloud Dataflow',
aggregation: aggregationFor(intervals, 1_700),
},
{
id: 'Compute Engine',
aggregation: aggregationFor(intervals, 350),
},
{
id: 'Cloud Storage',
aggregation: aggregationFor(intervals, 1_300),
},
{
id: 'BigQuery',
aggregation: aggregationFor(intervals, 2_000),
},
{
id: 'Cloud SQL',
aggregation: aggregationFor(intervals, 750),
},
{
id: 'Cloud Spanner',
aggregation: aggregationFor(intervals, 50),
},
{
id: 'Cloud Pub/Sub',
aggregation: aggregationFor(intervals, 1_000),
},
{
id: 'Cloud Bigtable',
aggregation: aggregationFor(intervals, 250),
},
];
export const getGroupedProjects = (intervals: string) => [
{
id: 'project-a',
aggregation: aggregationFor(intervals, 1_700),
},
{
id: 'project-b',
aggregation: aggregationFor(intervals, 350),
},
{
id: 'project-c',
aggregation: aggregationFor(intervals, 1_300),
},
];
@@ -22,7 +22,7 @@ import {
Duration,
Cost,
} from '../types';
import { MockAggregatedDailyCosts, trendlineOf, changeOf } from './mockData';
import { MockAggregatedDailyCosts, trendlineOf, changeOf } from '../testUtils';
const GrowthMap = {
[GrowthType.Negligible]: 'negligible growth',