Merge pull request #4786 from backstage/cost-insights-migrate-example
Cost insights migrate example
This commit is contained in:
@@ -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);
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-3
@@ -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
|
||||
|
||||
+2
-2
@@ -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 = [
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+18
-15
@@ -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> {
|
||||
+9
-9
@@ -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}
|
||||
+3
-3
@@ -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,
|
||||
+4
-5
@@ -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,
|
||||
+1
-1
@@ -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';
|
||||
+9
-9
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
+31
-357
@@ -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',
|
||||
);
|
||||
-12
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user