diff --git a/plugins/cost-insights/dev/index.tsx b/plugins/cost-insights/dev/index.tsx index e4732fc364..8cfa12fc18 100644 --- a/plugins/cost-insights/dev/index.tsx +++ b/plugins/cost-insights/dev/index.tsx @@ -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() diff --git a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx index 05a10bb2f0..35a0248ef5 100644 --- a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx +++ b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx @@ -23,7 +23,7 @@ import { MockCurrencyProvider, MockConfigProvider, MockBillingDateProvider, -} from '../utils/tests'; +} from '../testUtils'; const mockData: ProjectGrowthData = { project: 'test-project', diff --git a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx index 95e213f06a..00b8332cd9 100644 --- a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx +++ b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx @@ -23,7 +23,7 @@ import { MockCurrencyProvider, MockConfigProvider, MockBillingDateProvider, -} from '../utils/tests'; +} from '../testUtils'; const mockData: UnlabeledDataflowData = { periodStart: '2021-02-01', diff --git a/plugins/cost-insights/src/alerts/index.ts b/plugins/cost-insights/src/alerts/index.ts index b55c012a60..a732083454 100644 --- a/plugins/cost-insights/src/alerts/index.ts +++ b/plugins/cost-insights/src/alerts/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/components/ActionItems/ActionItemCard.test.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItemCard.test.tsx index b72f050e5b..70ae44bcdc 100644 --- a/plugins/cost-insights/src/components/ActionItems/ActionItemCard.test.tsx +++ b/plugins/cost-insights/src/components/ActionItems/ActionItemCard.test.tsx @@ -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'; diff --git a/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx index 677ba99c6a..39b8d1ad05 100644 --- a/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx +++ b/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx @@ -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({children}); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx index 98f75b1d41..5509d8f1ea 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx @@ -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( diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx index 937e099356..093ca1ff0c 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx @@ -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: diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx index 830db19fcd..ab39e0364e 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx @@ -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', diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx index 4e40a5ddda..cd031f9d1b 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx @@ -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'; diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index 12668140b6..f8c06845c9 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -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); diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx index f94ab48c6f..9b53cb9d1a 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx @@ -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[] = [ diff --git a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx index a910d43c22..24204abd95 100644 --- a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx @@ -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(); diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx index 6fca8f67c9..3ff4d31135 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx @@ -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 = { diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx index ae513c04e0..0c43e4d403 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx @@ -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[]); diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx index 3719bc8f6c..f782659ff2 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx @@ -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 diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index a2f87b1d86..cd171feba6 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -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 diff --git a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx index 0e24f042bc..52f334c4cb 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx @@ -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'; diff --git a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx index 16270429e8..1bd57a9eed 100644 --- a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx +++ b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx @@ -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 = [ diff --git a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx index df38c78a03..5aea9be548 100644 --- a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx +++ b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx @@ -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( diff --git a/plugins/cost-insights/src/components/index.ts b/plugins/cost-insights/src/components/index.ts index c390666419..4388250d31 100644 --- a/plugins/cost-insights/src/components/index.ts +++ b/plugins/cost-insights/src/components/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx b/plugins/cost-insights/src/example/alerts/KubernetesMigrationAlert.tsx similarity index 86% rename from plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx rename to plugins/cost-insights/src/example/alerts/KubernetesMigrationAlert.tsx index 31a4d7e0e9..4878462f62 100644 --- a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx +++ b/plugins/cost-insights/src/example/alerts/KubernetesMigrationAlert.tsx @@ -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; } -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 ( - , + options: AlertOptions, ): Promise { const alerts = await this.api.getAlerts(options.group); return new Promise(resolve => diff --git a/plugins/cost-insights/src/example/alerts/index.ts b/plugins/cost-insights/src/example/alerts/index.ts new file mode 100644 index 0000000000..eadd8d7ef4 --- /dev/null +++ b/plugins/cost-insights/src/example/alerts/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/example/client.ts similarity index 96% rename from plugins/cost-insights/src/client.ts rename to plugins/cost-insights/src/example/client.ts index 2e70170fc1..00d0a25233 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/example/client.ts @@ -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 { diff --git a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationAlertCard.tsx similarity index 75% rename from plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx rename to plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationAlertCard.tsx index 56d636d8aa..3cba2b7e4d 100644 --- a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx +++ b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationAlertCard.tsx @@ -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 ( - - ; }; -export const MigrationBarChart = ({ +export const KubernetesMigrationBarChart = ({ currentProduct, comparedProduct, services, diff --git a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChartLegend.tsx b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx similarity index 89% rename from plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChartLegend.tsx rename to plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx index a84e865e28..2c74535d0d 100644 --- a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChartLegend.tsx +++ b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx @@ -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, diff --git a/plugins/cost-insights/src/components/MigrationAlertCard/index.ts b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/index.ts similarity index 90% rename from plugins/cost-insights/src/components/MigrationAlertCard/index.ts rename to plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/index.ts index 4feb749c27..65f0c097f3 100644 --- a/plugins/cost-insights/src/components/MigrationAlertCard/index.ts +++ b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { MigrationAlertCard } from './MigrationAlertCard'; +export * from './KubernetesMigrationAlertCard'; diff --git a/plugins/cost-insights/src/example/components/index.ts b/plugins/cost-insights/src/example/components/index.ts new file mode 100644 index 0000000000..65f0c097f3 --- /dev/null +++ b/plugins/cost-insights/src/example/components/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/forms/MigrationDismissForm.tsx b/plugins/cost-insights/src/example/forms/KubernetesMigrationDismissForm.tsx similarity index 86% rename from plugins/cost-insights/src/forms/MigrationDismissForm.tsx rename to plugins/cost-insights/src/example/forms/KubernetesMigrationDismissForm.tsx index ce71af3a37..835577787c 100644 --- a/plugins/cost-insights/src/forms/MigrationDismissForm.tsx +++ b/plugins/cost-insights/src/example/forms/KubernetesMigrationDismissForm.tsx @@ -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(alert.data.services); diff --git a/plugins/cost-insights/src/example/forms/index.ts b/plugins/cost-insights/src/example/forms/index.ts new file mode 100644 index 0000000000..2ad52201f9 --- /dev/null +++ b/plugins/cost-insights/src/example/forms/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/example/index.ts b/plugins/cost-insights/src/example/index.ts new file mode 100644 index 0000000000..c27f03bdda --- /dev/null +++ b/plugins/cost-insights/src/example/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/forms/index.ts b/plugins/cost-insights/src/forms/index.ts index 5b8384a293..4692bc1de3 100644 --- a/plugins/cost-insights/src/forms/index.ts +++ b/plugins/cost-insights/src/forms/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 0ae9d05b09..2d12eec9d9 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/testUtils/alerts.ts b/plugins/cost-insights/src/testUtils/alerts.ts new file mode 100644 index 0000000000..2e8e1b114a --- /dev/null +++ b/plugins/cost-insights/src/testUtils/alerts.ts @@ -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 = (alert: T) => T; +type mockEntityRenderer = (entity: T) => T; + +export const createMockUnlabeledDataflowData = ( + callback?: mockAlertRenderer, +): 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 => { + 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 => { + 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 }; +}; diff --git a/plugins/cost-insights/src/testUtils/config.ts b/plugins/cost-insights/src/testUtils/config.ts new file mode 100644 index 0000000000..7d48121506 --- /dev/null +++ b/plugins/cost-insights/src/testUtils/config.ts @@ -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 = { + keys: () => Object.keys(MockProductTypes), +}; + +export const MockMetricsConfig: Partial = { + getOptionalString: () => 'daily-cost', + keys: () => ['daily-cost'], +}; + +export const MockCostInsightsConfig: Partial = { + getConfig: () => MockProductsConfig as Config, + getOptionalConfig: () => MockMetricsConfig as Config, +}; diff --git a/plugins/cost-insights/src/testUtils/filters.ts b/plugins/cost-insights/src/testUtils/filters.ts new file mode 100644 index 0000000000..e401b1f2e7 --- /dev/null +++ b/plugins/cost-insights/src/testUtils/filters.ts @@ -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 })); diff --git a/plugins/cost-insights/src/testUtils/index.ts b/plugins/cost-insights/src/testUtils/index.ts new file mode 100644 index 0000000000..a1748b15b2 --- /dev/null +++ b/plugins/cost-insights/src/testUtils/index.ts @@ -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'; diff --git a/plugins/cost-insights/src/testUtils/loading.ts b/plugins/cost-insights/src/testUtils/loading.ts new file mode 100644 index 0000000000..6c6f761ee8 --- /dev/null +++ b/plugins/cost-insights/src/testUtils/loading.ts @@ -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, +); diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/testUtils/mockData.ts similarity index 58% rename from plugins/cost-insights/src/utils/mockData.ts rename to plugins/cost-insights/src/testUtils/mockData.ts index d4ac4cd2df..5149da278d 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/testUtils/mockData.ts @@ -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 = (alert: T) => T; -type mockEntityRenderer = (entity: T) => T; - -type IntervalFields = { - duration: Duration; - endDate: string; -}; - -export const createMockEntity = ( - callback?: mockEntityRenderer, -): 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 => { - const defaultProduct: Product = { - kind: 'compute-engine', - name: 'Compute Engine', - }; - if (typeof callback === 'function') { - return callback({ ...defaultProduct }); - } - return { ...defaultProduct }; -}; - -export const createMockProjectGrowthData = ( - callback?: mockAlertRenderer, -): 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 => { - 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 => { - 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 = { - '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 = { - keys: () => Object.keys(MockProductTypes), -}; - -export const MockMetricsConfig: Partial = { - getOptionalString: () => 'daily-cost', - keys: () => ['daily-cost'], -}; - -export const MockCostInsightsConfig: Partial = { - getConfig: () => MockProductsConfig as Config, - getOptionalConfig: () => MockMetricsConfig as Config, -}; - -export function trendlineOf(aggregation: DateAggregation[]): Trendline { - const data: ReadonlyArray = 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( - /\/(?P\d+[DM])\/(?\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), - }, -]; diff --git a/plugins/cost-insights/src/testUtils/products.ts b/plugins/cost-insights/src/testUtils/products.ts new file mode 100644 index 0000000000..0ecac46244 --- /dev/null +++ b/plugins/cost-insights/src/testUtils/products.ts @@ -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 = (entity: T) => T; + +export const createMockEntity = ( + callback?: mockEntityRenderer, +): 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 => { + const defaultProduct: Product = { + kind: 'compute-engine', + name: 'Compute Engine', + }; + if (typeof callback === 'function') { + return callback({ ...defaultProduct }); + } + return { ...defaultProduct }; +}; + +export const MockProductTypes: Record = { + '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', +); diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/testUtils/providers.tsx similarity index 92% rename from plugins/cost-insights/src/utils/tests.tsx rename to plugins/cost-insights/src/testUtils/providers.tsx index aae2800573..1097d86be4 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -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 = PropsWithChildren>; export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }]; diff --git a/plugins/cost-insights/src/testUtils/testUtils.ts b/plugins/cost-insights/src/testUtils/testUtils.ts new file mode 100644 index 0000000000..37913f635e --- /dev/null +++ b/plugins/cost-insights/src/testUtils/testUtils.ts @@ -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( + /\/(?P\d+[DM])\/(?\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 = 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), + }, +]; diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index 9516dfe270..7208e9bc4f 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -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',