Enable custom alert types in Cost Insights
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': minor
|
||||
---
|
||||
|
||||
Enable custom alert types in Cost Insights
|
||||
@@ -141,6 +141,7 @@ Raghunandan
|
||||
rankdir
|
||||
readme
|
||||
Readme
|
||||
Recharts
|
||||
Redash
|
||||
replicasets
|
||||
repo
|
||||
|
||||
@@ -16,13 +16,17 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
|
||||
import {
|
||||
CostInsightsApi,
|
||||
Alert,
|
||||
Cost,
|
||||
CostInsightsApi,
|
||||
Duration,
|
||||
Project,
|
||||
ProductCost,
|
||||
Group,
|
||||
ProductCost,
|
||||
Project,
|
||||
ProjectGrowthAlert,
|
||||
ProjectGrowthData,
|
||||
UnlabeledDataflowAlert,
|
||||
UnlabeledDataflowData,
|
||||
} from '@backstage/plugin-cost-insights';
|
||||
|
||||
export class ExampleCostInsightsClient implements CostInsightsApi {
|
||||
@@ -176,32 +180,44 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
|
||||
}
|
||||
|
||||
async getAlerts(group: string): Promise<Alert[]> {
|
||||
const alerts: Alert[] = await this.request({ group }, [
|
||||
{
|
||||
id: 'projectGrowth',
|
||||
project: 'example-project',
|
||||
periodStart: 'Q1 2020',
|
||||
periodEnd: 'Q2 2020',
|
||||
aggregation: [60_000, 120_000],
|
||||
change: {
|
||||
ratio: 1,
|
||||
amount: 60000,
|
||||
},
|
||||
products: [
|
||||
{
|
||||
id: 'Compute Engine',
|
||||
aggregation: [58_000, 118_000],
|
||||
},
|
||||
{
|
||||
id: 'Cloud Dataflow',
|
||||
aggregation: [1200, 1500],
|
||||
},
|
||||
{
|
||||
id: 'Cloud Storage',
|
||||
aggregation: [800, 500],
|
||||
},
|
||||
],
|
||||
const projectGrowthData: ProjectGrowthData = {
|
||||
project: 'example-project',
|
||||
periodStart: 'Q2 2020',
|
||||
periodEnd: 'Q3 2020',
|
||||
aggregation: [60_000, 120_000],
|
||||
change: {
|
||||
ratio: 1,
|
||||
amount: 60000,
|
||||
},
|
||||
products: [
|
||||
{ id: 'Compute Engine', aggregation: [58_000, 118_000] },
|
||||
{ id: 'Cloud Dataflow', aggregation: [1200, 1500] },
|
||||
{ id: 'Cloud Storage', aggregation: [800, 500] },
|
||||
],
|
||||
};
|
||||
|
||||
const unlabeledDataflowData: UnlabeledDataflowData = {
|
||||
periodStart: '2020-09-01',
|
||||
periodEnd: '2020-09-30',
|
||||
labeledCost: 6_200,
|
||||
unlabeledCost: 7_000,
|
||||
projects: [
|
||||
{
|
||||
id: 'example-project-1',
|
||||
unlabeledCost: 5_000,
|
||||
labeledCost: 3_000,
|
||||
},
|
||||
{
|
||||
id: 'example-project-2',
|
||||
unlabeledCost: 2_000,
|
||||
labeledCost: 3_200,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const alerts: Alert[] = await this.request({ group }, [
|
||||
new ProjectGrowthAlert(projectGrowthData),
|
||||
new UnlabeledDataflowAlert(unlabeledDataflowData),
|
||||
]);
|
||||
|
||||
return alerts;
|
||||
|
||||
@@ -104,3 +104,11 @@ costInsights:
|
||||
metricC:
|
||||
name: Metric C
|
||||
```
|
||||
|
||||
## Alerts
|
||||
|
||||
The CostInsightsApi `getAlerts` method may return any type of alert or recommendation (called collectively "Action Items" in Cost Insights) that implements the [Alert type](https://github.com/spotify/backstage/blob/master/plugins/cost-insights/src/types/Alert.tsx). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations.
|
||||
|
||||
The Alert type includes an `element` field to supply the JSX Element that will be rendered in the Cost Insights "Action Items" section; we recommend using Backstage's [InfoCard](https://backstage.io/storybook/?path=/story/layout-information-card--default) and [Recharts](http://recharts.org/en-US/) to show actionable visualizations.
|
||||
|
||||
The Alert `url` should link to documentation or instructions for resolving the alert.
|
||||
|
||||
@@ -17,19 +17,18 @@
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import AlertActionCard from './AlertActionCard';
|
||||
import { AlertType, ProjectGrowthAlert } from '../../types';
|
||||
import { getAlertText } from '../../utils/alerts';
|
||||
import { ProjectGrowthAlert, ProjectGrowthData } from '../../types';
|
||||
import { MockScrollProvider } from '../../utils/tests';
|
||||
|
||||
const alert = {
|
||||
id: AlertType.ProjectGrowth,
|
||||
const data: ProjectGrowthData = {
|
||||
aggregation: [500000.8, 970502.8],
|
||||
project: 'test-project',
|
||||
periodStart: '2019-10-01',
|
||||
periodEnd: '2020-03-31',
|
||||
change: { ratio: 120, amount: 120000 },
|
||||
products: [],
|
||||
} as ProjectGrowthAlert;
|
||||
};
|
||||
const alert = new ProjectGrowthAlert(data);
|
||||
|
||||
describe('<AlertActionCard/>', () => {
|
||||
it('Renders an alert', async () => {
|
||||
@@ -40,11 +39,7 @@ describe('<AlertActionCard/>', () => {
|
||||
);
|
||||
|
||||
expect(rendered.getByText('1')).toBeInTheDocument();
|
||||
const text = getAlertText(alert);
|
||||
expect(text).toBeDefined();
|
||||
if (text) {
|
||||
expect(rendered.getByText(text.title)).toBeInTheDocument();
|
||||
expect(rendered.getByText(text.subtitle)).toBeInTheDocument();
|
||||
}
|
||||
expect(rendered.getByText(alert.title)).toBeInTheDocument();
|
||||
expect(rendered.getByText(alert.subtitle)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,9 @@ import React from 'react';
|
||||
import { Avatar, Card, CardHeader } from '@material-ui/core';
|
||||
import { useScroll } from '../../hooks';
|
||||
import { Alert } from '../../types';
|
||||
import { getAlertText, getAlertNavigation } from '../../utils/alerts';
|
||||
import {
|
||||
useAlertActionCardStyles as useStyles,
|
||||
useAlertActionCardHeader as useHeaderStyles,
|
||||
useAlertActionCardStyles as useStyles,
|
||||
} from '../../utils/styles';
|
||||
|
||||
type AlertActionCardProps = {
|
||||
@@ -29,9 +28,8 @@ type AlertActionCardProps = {
|
||||
};
|
||||
|
||||
const AlertActionCard = ({ alert, number }: AlertActionCardProps) => {
|
||||
const { scrollIntoView } = useScroll(getAlertNavigation(alert, number));
|
||||
const { scrollIntoView } = useScroll(`alert-${number}`);
|
||||
const headerClasses = useHeaderStyles();
|
||||
const text = getAlertText(alert);
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
@@ -39,8 +37,8 @@ const AlertActionCard = ({ alert, number }: AlertActionCardProps) => {
|
||||
<CardHeader
|
||||
classes={headerClasses}
|
||||
avatar={<Avatar className={classes.avatar}>{number}</Avatar>}
|
||||
title={text?.title}
|
||||
subheader={text?.subtitle}
|
||||
title={alert.title}
|
||||
subheader={alert.subtitle}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ type AlertActionCardList = {
|
||||
const AlertActionCardList: FC<AlertActionCardList> = ({ alerts }) => (
|
||||
<Paper>
|
||||
{alerts.map((alert, index) => (
|
||||
<Fragment key={`${alert.id}-${index}`}>
|
||||
<Fragment key={`alert-${index}`}>
|
||||
<AlertActionCard alert={alert} number={index + 1} />
|
||||
{index < alerts.length - 1 && <Divider variant="fullWidth" />}
|
||||
</Fragment>
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Grid } from '@material-ui/core';
|
||||
import AlertInsightsSection from './AlertInsightsSection';
|
||||
import AlertInsightsHeader from './AlertInsightsHeader';
|
||||
import { Alert } from '../../types';
|
||||
import { renderAlert } from '../../utils/alerts';
|
||||
|
||||
const title = "Your team's action items";
|
||||
const subtitle =
|
||||
@@ -37,11 +36,7 @@ const AlertInsights = ({ alerts }: AlertInsightsProps) => (
|
||||
<Grid item container direction="column" spacing={4}>
|
||||
{alerts.map((alert, index) => (
|
||||
<Grid item key={`alert-card-${index}`}>
|
||||
<AlertInsightsSection
|
||||
alert={alert}
|
||||
number={index + 1}
|
||||
render={renderAlert}
|
||||
/>
|
||||
<AlertInsightsSection alert={alert} number={index + 1} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -16,45 +16,28 @@
|
||||
import React from 'react';
|
||||
import { Box, Button } from '@material-ui/core';
|
||||
import AlertInsightsSectionHeader from './AlertInsightsSectionHeader';
|
||||
import {
|
||||
getAlertButtonText,
|
||||
getAlertText,
|
||||
getAlertUrl,
|
||||
} from '../../utils/alerts';
|
||||
import { Alert, Currency } from '../../types';
|
||||
import { useCurrency } from '../../hooks';
|
||||
import { Alert } from '../../types';
|
||||
|
||||
type AlertInsightsSectionProps = {
|
||||
alert: Alert;
|
||||
number: number;
|
||||
render: (alert: Alert, currency: Currency) => JSX.Element;
|
||||
};
|
||||
|
||||
const AlertInsightsSection = ({
|
||||
alert,
|
||||
number,
|
||||
render,
|
||||
}: AlertInsightsSectionProps) => {
|
||||
const [currency] = useCurrency();
|
||||
const text = getAlertText(alert);
|
||||
const url = getAlertUrl(alert);
|
||||
const buttonText = getAlertButtonText(alert);
|
||||
|
||||
const AlertInsightsSection = ({ alert, number }: AlertInsightsSectionProps) => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<AlertInsightsSectionHeader
|
||||
alert={alert}
|
||||
title={text.title}
|
||||
subtitle={text.subtitle}
|
||||
title={alert.title}
|
||||
subtitle={alert.subtitle}
|
||||
number={number}
|
||||
/>
|
||||
<Box textAlign="left" mt={0} mb={4}>
|
||||
<Button variant="contained" color="primary" href={url}>
|
||||
{buttonText}
|
||||
<Button variant="contained" color="primary" href={alert.url}>
|
||||
{alert.buttonText || 'View Instructions'}
|
||||
</Button>
|
||||
{/* <Button color="primary">Dismiss notification</Button> */}
|
||||
</Box>
|
||||
{render(alert, currency)}
|
||||
{alert.element}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,26 +15,22 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar, Box, Typography, Grid } from '@material-ui/core';
|
||||
import { Alert } from '../../types';
|
||||
import { getAlertNavigation } from '../../utils/alerts';
|
||||
import { Avatar, Box, Grid, Typography } from '@material-ui/core';
|
||||
import { useAlertInsightsSectionStyles as useStyles } from '../../utils/styles';
|
||||
import { useScroll } from '../../hooks';
|
||||
|
||||
type AlertInsightsSectionHeaderProps = {
|
||||
alert: Alert;
|
||||
number: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
|
||||
const AlertInsightsSectionHeader = ({
|
||||
alert,
|
||||
number,
|
||||
title,
|
||||
subtitle,
|
||||
}: AlertInsightsSectionHeaderProps) => {
|
||||
const { ScrollAnchor } = useScroll(getAlertNavigation(alert, number));
|
||||
const { ScrollAnchor } = useScroll(`alert-${number}`);
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Box position="relative" mb={3} textAlign="left">
|
||||
|
||||
+24
-15
@@ -19,6 +19,7 @@ import { Box, Button, Container, makeStyles } from '@material-ui/core';
|
||||
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
|
||||
import { Header, Page, pageTheme } from '@backstage/core';
|
||||
import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider';
|
||||
import { ConfigProvider, CurrencyProvider } from '../../hooks';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@@ -39,21 +40,29 @@ const AlertInstructionsLayout = ({
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<CostInsightsThemeProvider>
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="Cost Insights" pageTitleOverride={title} type="Tool" />
|
||||
<Container maxWidth="md" disableGutters className={classes.root}>
|
||||
<Box mb={3}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ChevronLeftIcon />}
|
||||
href="/cost-insights"
|
||||
>
|
||||
Back to Cost Insights
|
||||
</Button>
|
||||
</Box>
|
||||
{children}
|
||||
</Container>
|
||||
</Page>
|
||||
<ConfigProvider>
|
||||
<CurrencyProvider>
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Cost Insights"
|
||||
pageTitleOverride={title}
|
||||
type="Tool"
|
||||
/>
|
||||
<Container maxWidth="md" disableGutters className={classes.root}>
|
||||
<Box mb={3}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ChevronLeftIcon />}
|
||||
href="/cost-insights"
|
||||
>
|
||||
Back to Cost Insights
|
||||
</Button>
|
||||
</Box>
|
||||
{children}
|
||||
</Container>
|
||||
</Page>
|
||||
</CurrencyProvider>
|
||||
</ConfigProvider>
|
||||
</CostInsightsThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import ProjectGrowthAlertCard from './ProjectGrowthAlertCard';
|
||||
import { createMockProjectGrowthAlert } from '../../utils/mockData';
|
||||
import { createMockProjectGrowthData } from '../../utils/mockData';
|
||||
import { MockCurrencyProvider, MockConfigProvider } from '../../utils/tests';
|
||||
import { AlertCost, defaultCurrencies, findAlways } from '../../types';
|
||||
|
||||
@@ -29,8 +29,8 @@ const MockAlertCosts: AlertCost[] = [
|
||||
{ id: 'test-id-2', aggregation: [235, 400] },
|
||||
];
|
||||
|
||||
const MockProjectGrowthAlert = createMockProjectGrowthAlert(alert => ({
|
||||
...alert,
|
||||
const MockProjectGrowthAlert = createMockProjectGrowthData(data => ({
|
||||
...data,
|
||||
project: MockProject,
|
||||
products: MockAlertCosts,
|
||||
}));
|
||||
|
||||
+2
-2
@@ -19,11 +19,11 @@ import { Box } from '@material-ui/core';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import ResourceGrowthBarChart from '../ResourceGrowthBarChart';
|
||||
import ResourceGrowthBarChartLegend from '../ResourceGrowthBarChartLegend';
|
||||
import { Duration, ProjectGrowthAlert } from '../../types';
|
||||
import { Duration, ProjectGrowthData } from '../../types';
|
||||
import { pluralOf } from '../../utils/grammar';
|
||||
|
||||
type ProjectGrowthAlertProps = {
|
||||
alert: ProjectGrowthAlert;
|
||||
alert: ProjectGrowthData;
|
||||
};
|
||||
|
||||
const ProjectGrowthAlertCard = ({ alert }: ProjectGrowthAlertProps) => {
|
||||
|
||||
+5
-5
@@ -18,20 +18,19 @@ import React from 'react';
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import AlertInstructionsLayout from '../AlertInstructionsLayout';
|
||||
import ProjectGrowthAlertCard from '../ProjectGrowthAlertCard';
|
||||
import {
|
||||
AlertType,
|
||||
Alert,
|
||||
Duration,
|
||||
Entity,
|
||||
Product,
|
||||
ProjectGrowthAlert,
|
||||
ProjectGrowthData,
|
||||
} from '../../types';
|
||||
import ResourceGrowthBarChartLegend from '../ResourceGrowthBarChartLegend';
|
||||
import ResourceGrowthBarChart from '../ResourceGrowthBarChart';
|
||||
|
||||
const ProjectGrowthInstructionsPage = () => {
|
||||
const projectGrowthAlert: ProjectGrowthAlert = {
|
||||
id: AlertType.ProjectGrowth,
|
||||
const alertData: ProjectGrowthData = {
|
||||
project: 'example-project',
|
||||
periodStart: 'Q1 2020',
|
||||
periodEnd: 'Q2 2020',
|
||||
@@ -55,6 +54,7 @@ const ProjectGrowthInstructionsPage = () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const projectGrowthAlert: Alert = new ProjectGrowthAlert(alertData);
|
||||
|
||||
const product: Product = {
|
||||
kind: 'ComputeEngine',
|
||||
@@ -135,7 +135,7 @@ const ProjectGrowthInstructionsPage = () => {
|
||||
comparison of cloud products over the examined time period:
|
||||
</Typography>
|
||||
<Box mt={2} mb={2}>
|
||||
<ProjectGrowthAlertCard alert={projectGrowthAlert} />
|
||||
{projectGrowthAlert.element}
|
||||
</Box>
|
||||
<Typography paragraph>
|
||||
This allows you to quickly see which cloud products contributed to the
|
||||
|
||||
+7
-7
@@ -17,23 +17,23 @@
|
||||
import React from 'react';
|
||||
import UnlabeledDataflowAlertCard from './UnlabeledDataflowAlertCard';
|
||||
import {
|
||||
createMockUnlabeledDataflowAlert,
|
||||
createMockUnlabeledDataflowData,
|
||||
createMockUnlabeledDataflowAlertProject,
|
||||
} from '../../utils/mockData';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
const MockUnlabeledDataflowAlertMultipleProjects = createMockUnlabeledDataflowAlert(
|
||||
alert => ({
|
||||
...alert,
|
||||
const MockUnlabeledDataflowAlertMultipleProjects = createMockUnlabeledDataflowData(
|
||||
data => ({
|
||||
...data,
|
||||
projects: [...Array(10)].map(() =>
|
||||
createMockUnlabeledDataflowAlertProject(),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const MockUnlabeledDataflowAlertSingleProject = createMockUnlabeledDataflowAlert(
|
||||
alert => ({
|
||||
...alert,
|
||||
const MockUnlabeledDataflowAlertSingleProject = createMockUnlabeledDataflowData(
|
||||
data => ({
|
||||
...data,
|
||||
projects: [...Array(1)].map(() =>
|
||||
createMockUnlabeledDataflowAlertProject(),
|
||||
),
|
||||
|
||||
+2
-2
@@ -19,11 +19,11 @@ import { Box } from '@material-ui/core';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import UnlabeledDataflowBarChart from '../UnlabeledDataflowBarChart';
|
||||
import UnlabeledDataflowBarChartLegend from '../UnlabeledDataflowBarChartLegend';
|
||||
import { UnlabeledDataflowAlert } from '../../types';
|
||||
import { UnlabeledDataflowData } from '../../types';
|
||||
import { pluralOf } from '../../utils/grammar';
|
||||
|
||||
type UnlabeledDataflowAlertProps = {
|
||||
alert: UnlabeledDataflowAlert;
|
||||
alert: UnlabeledDataflowData;
|
||||
};
|
||||
|
||||
const UnlabeledDataflowAlertCard = ({ alert }: UnlabeledDataflowAlertProps) => {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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 { ChangeStatistic } from './ChangeStatistic';
|
||||
import { Maybe } from './Maybe';
|
||||
|
||||
export type Alert = ProjectGrowthAlert | UnlabeledDataflowAlert;
|
||||
|
||||
export interface AlertProps {
|
||||
alert: Alert;
|
||||
}
|
||||
|
||||
export enum AlertType {
|
||||
ProjectGrowth = 'projectGrowth',
|
||||
UnlabeledDataflow = 'unlabeledDataflow',
|
||||
}
|
||||
|
||||
export interface AlertCost {
|
||||
id: string;
|
||||
aggregation: [number, number];
|
||||
}
|
||||
|
||||
export interface ResourceData {
|
||||
previous: number;
|
||||
current: number;
|
||||
name: Maybe<string>;
|
||||
}
|
||||
|
||||
export interface BarChartData {
|
||||
previousFill: string;
|
||||
currentFill: string;
|
||||
previousName: string;
|
||||
currentName: string;
|
||||
}
|
||||
|
||||
export enum DataKey {
|
||||
Previous = 'previous',
|
||||
Current = 'current',
|
||||
Name = 'name',
|
||||
}
|
||||
|
||||
export interface ProjectGrowthAlert {
|
||||
id: AlertType.ProjectGrowth;
|
||||
project: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
aggregation: [number, number];
|
||||
change: ChangeStatistic;
|
||||
products: Array<AlertCost>;
|
||||
}
|
||||
|
||||
export interface UnlabeledDataflowAlert {
|
||||
id: AlertType.UnlabeledDataflow;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
projects: Array<UnlabeledDataflowAlertProject>;
|
||||
unlabeledCost: number;
|
||||
labeledCost: number;
|
||||
}
|
||||
|
||||
export interface UnlabeledDataflowAlertProject {
|
||||
id: string;
|
||||
unlabeledCost: number;
|
||||
labeledCost: number;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ChangeStatistic } from './ChangeStatistic';
|
||||
import { Maybe } from './Maybe';
|
||||
import UnlabeledDataflowAlertCard from '../components/UnlabeledDataflowAlertCard';
|
||||
import ProjectGrowthAlertCard from '../components/ProjectGrowthAlertCard';
|
||||
|
||||
/**
|
||||
* Generic alert type with required fields for display. The `element` field will be rendered in
|
||||
* the Cost Insights "Action Items" section. This should use data fetched in the CostInsightsApi
|
||||
* implementation to render an InfoCard or other visualization.
|
||||
*/
|
||||
export type Alert = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
url: string;
|
||||
buttonText?: string; // Default: View Instructions
|
||||
element: JSX.Element;
|
||||
};
|
||||
|
||||
export interface AlertCost {
|
||||
id: string;
|
||||
aggregation: [number, number];
|
||||
}
|
||||
|
||||
export interface ResourceData {
|
||||
previous: number;
|
||||
current: number;
|
||||
name: Maybe<string>;
|
||||
}
|
||||
|
||||
export interface BarChartData {
|
||||
previousFill: string;
|
||||
currentFill: string;
|
||||
previousName: string;
|
||||
currentName: string;
|
||||
}
|
||||
|
||||
export enum DataKey {
|
||||
Previous = 'previous',
|
||||
Current = 'current',
|
||||
Name = 'name',
|
||||
}
|
||||
|
||||
/**
|
||||
* The alerts below are examples of Alert implementation; the CostInsightsApi permits returning
|
||||
* any implementation of the Alert type, so adopters can create their own. The CostInsightsApi
|
||||
* fetches alert data from the backend, then creates Alert classes with the data.
|
||||
*/
|
||||
export interface ProjectGrowthData {
|
||||
project: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
aggregation: [number, number];
|
||||
change: ChangeStatistic;
|
||||
products: Array<AlertCost>;
|
||||
}
|
||||
|
||||
export class ProjectGrowthAlert implements Alert {
|
||||
data: ProjectGrowthData;
|
||||
|
||||
constructor(data: ProjectGrowthData) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return `Investigate cost growth in project ${this.data.project}`;
|
||||
}
|
||||
|
||||
subtitle =
|
||||
'Cost growth outpacing business growth is unsustainable long-term.';
|
||||
url = '/cost-insights/investigating-growth';
|
||||
|
||||
get element() {
|
||||
return <ProjectGrowthAlertCard alert={this.data} />;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UnlabeledDataflowData {
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
projects: Array<UnlabeledDataflowAlertProject>;
|
||||
unlabeledCost: number;
|
||||
labeledCost: number;
|
||||
}
|
||||
|
||||
export class UnlabeledDataflowAlert implements Alert {
|
||||
data: UnlabeledDataflowData;
|
||||
|
||||
constructor(data: UnlabeledDataflowData) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
title = 'Add labels to workflows';
|
||||
subtitle =
|
||||
'Labels show in billing data, enabling cost insights for each workflow.';
|
||||
url = '/cost-insights/labeling-jobs';
|
||||
|
||||
get element() {
|
||||
return <UnlabeledDataflowAlertCard alert={this.data} />;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UnlabeledDataflowAlertProject {
|
||||
id: string;
|
||||
unlabeledCost: number;
|
||||
labeledCost: number;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Alert, AlertType, assertNever } from '../types';
|
||||
import ProjectGrowthAlertCard from '../components/ProjectGrowthAlertCard';
|
||||
import UnlabeledDataflowAlertCard from '../components/UnlabeledDataflowAlertCard';
|
||||
|
||||
export function getAlertText(alert: Alert) {
|
||||
switch (alert.id) {
|
||||
case AlertType.ProjectGrowth:
|
||||
return {
|
||||
title: `Investigate cost growth in project ${alert.project}`,
|
||||
subtitle:
|
||||
'Cost growth outpacing business growth is unsustainable long-term.',
|
||||
} as AlertText;
|
||||
case AlertType.UnlabeledDataflow:
|
||||
return {
|
||||
title: 'Add labels to workflows',
|
||||
subtitle:
|
||||
'Labels show in billing data, enabling cost insights for each workflow.',
|
||||
};
|
||||
default:
|
||||
return assertNever(alert);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAlertUrl(alert: Alert) {
|
||||
switch (alert.id) {
|
||||
case AlertType.ProjectGrowth:
|
||||
return '/cost-insights/investigating-growth' as AlertUrl;
|
||||
case AlertType.UnlabeledDataflow:
|
||||
return '/cost-insights/labeling-jobs' as AlertUrl;
|
||||
default:
|
||||
return assertNever(alert);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAlertButtonText(alert: Alert) {
|
||||
switch (alert.id) {
|
||||
case AlertType.ProjectGrowth:
|
||||
case AlertType.UnlabeledDataflow:
|
||||
return 'View Instructions' as AlertButtonText;
|
||||
default:
|
||||
return assertNever(alert);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAlertNavigation(alert: Alert, number: number) {
|
||||
return `${alert.id}-${number}`;
|
||||
}
|
||||
|
||||
export function renderAlert(alert: Alert) {
|
||||
switch (alert.id) {
|
||||
case AlertType.ProjectGrowth:
|
||||
return <ProjectGrowthAlertCard alert={alert} />;
|
||||
case AlertType.UnlabeledDataflow:
|
||||
return <UnlabeledDataflowAlertCard alert={alert} />;
|
||||
default:
|
||||
return assertNever(alert);
|
||||
}
|
||||
}
|
||||
|
||||
export type AlertUrl = string;
|
||||
export type AlertButtonText = string;
|
||||
|
||||
export interface AlertText {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
@@ -15,17 +15,16 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AlertType,
|
||||
Entity,
|
||||
ProjectGrowthAlert,
|
||||
Product,
|
||||
UnlabeledDataflowAlert,
|
||||
UnlabeledDataflowAlertProject,
|
||||
getDefaultState,
|
||||
DefaultLoadingAction,
|
||||
Duration,
|
||||
ProductCost,
|
||||
Entity,
|
||||
findAlways,
|
||||
getDefaultState,
|
||||
Product,
|
||||
ProductCost,
|
||||
ProjectGrowthData,
|
||||
UnlabeledDataflowAlertProject,
|
||||
UnlabeledDataflowData,
|
||||
} from '../types';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigApi } from '@backstage/core';
|
||||
@@ -73,11 +72,10 @@ export const createMockProductCost = (
|
||||
return { ...defaultProduct };
|
||||
};
|
||||
|
||||
export const createMockProjectGrowthAlert = (
|
||||
callback?: mockAlertRenderer<ProjectGrowthAlert>,
|
||||
): ProjectGrowthAlert => {
|
||||
const defaultAlert: ProjectGrowthAlert = {
|
||||
id: AlertType.ProjectGrowth,
|
||||
export const createMockProjectGrowthData = (
|
||||
callback?: mockAlertRenderer<ProjectGrowthData>,
|
||||
): ProjectGrowthData => {
|
||||
const data: ProjectGrowthData = {
|
||||
project: 'test-project-growth-alert',
|
||||
periodStart: '2019-10-01',
|
||||
periodEnd: '2020-03-31',
|
||||
@@ -90,17 +88,16 @@ export const createMockProjectGrowthAlert = (
|
||||
};
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
return callback({ ...defaultAlert });
|
||||
return callback({ ...data });
|
||||
}
|
||||
|
||||
return { ...defaultAlert };
|
||||
return { ...data };
|
||||
};
|
||||
|
||||
export const createMockUnlabeledDataflowAlert = (
|
||||
callback?: mockAlertRenderer<UnlabeledDataflowAlert>,
|
||||
): UnlabeledDataflowAlert => {
|
||||
const defaultAlert: UnlabeledDataflowAlert = {
|
||||
id: AlertType.UnlabeledDataflow,
|
||||
export const createMockUnlabeledDataflowData = (
|
||||
callback?: mockAlertRenderer<UnlabeledDataflowData>,
|
||||
): UnlabeledDataflowData => {
|
||||
const data: UnlabeledDataflowData = {
|
||||
periodStart: '2020-05-01',
|
||||
periodEnd: '2020-06-1',
|
||||
projects: [],
|
||||
@@ -109,10 +106,10 @@ export const createMockUnlabeledDataflowAlert = (
|
||||
};
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
return callback({ ...defaultAlert });
|
||||
return callback({ ...data });
|
||||
}
|
||||
|
||||
return { ...defaultAlert };
|
||||
return { ...data };
|
||||
};
|
||||
|
||||
export const createMockUnlabeledDataflowAlertProject = (
|
||||
|
||||
Reference in New Issue
Block a user