migrate mocks to test utils

Signed-off-by: Ryan Vazquez <ryanv@spotify.com>
This commit is contained in:
Ryan Vazquez
2021-03-03 15:10:17 -05:00
parent e7be6a33f7
commit 9bcad72a24
18 changed files with 481 additions and 381 deletions
@@ -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';
@@ -24,8 +24,6 @@ import {
getGroupedProjects,
MockAggregatedDailyCosts,
trendlineOf,
} from '../../utils/mockData';
import {
MockBillingDateProvider,
MockConfigProvider,
MockFilterProvider,
@@ -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,
@@ -22,8 +22,6 @@ import {
createMockEntity,
mockDefaultLoadingState,
MockComputeEngine,
} from '../../utils/mockData';
import {
MockConfigProvider,
MockCostInsightsApiProvider,
MockCurrencyProvider,
@@ -17,8 +17,8 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ProjectGrowthAlertCard } from './ProjectGrowthAlertCard';
import { createMockProjectGrowthData } from '../../utils/mockData';
import {
createMockProjectGrowthData,
MockCurrencyProvider,
MockConfigProvider,
MockBillingDateProvider,
@@ -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(
+1 -1
View File
@@ -37,7 +37,7 @@ import {
getGroupedProducts,
getGroupedProjects,
trendlineOf,
} from '../utils/mockData';
} from '../testUtils';
export class ExampleCostInsightsClient implements CostInsightsApi {
private request(_: any, res: any): Promise<any> {
+1 -1
View File
@@ -23,7 +23,7 @@ export {
} from './plugin';
export { ExampleCostInsightsClient } from './example';
export { BarChart, LegendItem, CostGrowth } from './components';
export { MockConfigProvider, MockCurrencyProvider } from './testUtils';
export * from './api';
export * from './alerts';
export * from './types';
export * from './testUtils';
@@ -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 }));
+7 -1
View File
@@ -13,5 +13,11 @@
* 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',