Merge pull request #2789 from spotify/update-project-select

Add global project filter to Cost Insights overview page
This commit is contained in:
brendasukh
2020-10-09 11:00:04 -04:00
committed by GitHub
9 changed files with 110 additions and 63 deletions
@@ -27,6 +27,7 @@ import {
ProjectGrowthData,
UnlabeledDataflowAlert,
UnlabeledDataflowData,
Maybe,
} from '@backstage/plugin-cost-insights';
export class ExampleCostInsightsClient implements CostInsightsApi {
@@ -126,9 +127,38 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
product: string,
group: string,
duration: Duration,
project: Maybe<string>,
): Promise<ProductCost> {
const projectProductInsights = await this.request(
{ product, group, duration, project },
{
aggregation: [80_000, 110_000],
change: {
ratio: 0.375,
amount: 30_000,
},
entities: [
{
id: null, // entities with null ids will be appear as "Unlabeled" in product panels
aggregation: [45_000, 50_000],
},
{
id: 'entity-a',
aggregation: [15_000, 20_000],
},
{
id: 'entity-b',
aggregation: [20_000, 30_000],
},
{
id: 'entity-e',
aggregation: [0, 10_000],
},
],
},
);
const productInsights: ProductCost = await this.request(
{ product, group, duration },
{ product, group, duration, project },
{
aggregation: [200_000, 250_000],
change: {
@@ -176,7 +206,7 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
},
);
return productInsights;
return project ? projectProductInsights : productInsights;
}
async getAlerts(group: string): Promise<Alert[]> {
@@ -15,7 +15,15 @@
*/
import { createApiRef } from '@backstage/core';
import { Alert, Cost, Duration, Group, Project, ProductCost } from '../types';
import {
Alert,
Cost,
Duration,
Group,
Project,
ProductCost,
Maybe,
} from '../types';
export type CostInsightsApi = {
/**
@@ -87,6 +95,9 @@ export type CostInsightsApi = {
* in this product. The type of entity depends on the product - it may be deployed services,
* storage buckets, managed database instances, etc.
*
* If project is supplied, this should only return product costs for the given billing entity
* (project in GCP).
*
* The time period is supplied as a Duration rather than intervals, since this is always expected
* to return data for two bucketed time period (e.g. month vs month, or quarter vs quarter).
*
@@ -94,11 +105,13 @@ export type CostInsightsApi = {
* @param group
* @param duration A time duration, such as P1M. See the Duration type for a detailed explanation
* of how the durations are interpreted in Cost Insights.
* @param project (optional) The project id from getGroupProjects or query parameters
*/
getProductInsights(
product: string,
group: string,
duration: Duration,
project: Maybe<string>,
): Promise<ProductCost>;
/**
@@ -15,7 +15,7 @@
*/
import React, { useCallback, useEffect, useState } from 'react';
import { Box, Container, Divider, Grid } from '@material-ui/core';
import { Box, Container, Divider, Grid, Typography } from '@material-ui/core';
import { Progress, useApi, featureFlagsApiRef } from '@backstage/core';
import { default as MaterialAlert } from '@material-ui/lab/Alert';
import { costInsightsApiRef } from '../../api';
@@ -41,6 +41,7 @@ import {
} from '../../hooks';
import { Alert, Cost, intervalsOf, Maybe, Project } from '../../types';
import { mapLoadingToProps } from './selector';
import ProjectSelect from '../ProjectSelect';
const CostInsightsPage = () => {
const flags = useApi(featureFlagsApiRef).getFlags();
@@ -55,7 +56,7 @@ const CostInsightsPage = () => {
const [alerts, setAlerts] = useState<Maybe<Alert[]>>(null);
const [error, setError] = useState<Maybe<Error>>(null);
const { pageFilters } = useFilters(p => p);
const { pageFilters, setPageFilters } = useFilters(p => p);
const {
loadingActions,
loadingGroups,
@@ -63,6 +64,7 @@ const CostInsightsPage = () => {
dispatchInitial,
dispatchInsights,
dispatchNone,
dispatchReset,
} = useLoading(mapLoadingToProps);
/* eslint-disable react-hooks/exhaustive-deps */
@@ -75,8 +77,15 @@ const CostInsightsPage = () => {
const dispatchLoadingInitial = useCallback(dispatchInitial, []);
const dispatchLoadingInsights = useCallback(dispatchInsights, []);
const dispatchLoadingNone = useCallback(dispatchNone, []);
const dispatchLoadingReset = useCallback(dispatchReset, []);
/* eslint-enable react-hooks/exhaustive-deps */
const setProject = (project: Maybe<string>) =>
setPageFilters({
...pageFilters,
project: project === 'all' ? null : project,
});
useEffect(() => {
async function getInsights() {
setError(null);
@@ -165,6 +174,41 @@ const CostInsightsPage = () => {
);
}
const onProjectSelect = (project: Maybe<string>) => {
setProject(project);
dispatchLoadingReset(loadingActions);
};
const CostOverviewBanner = () => (
<Box
px={3}
marginTop={10}
display="flex"
flexDirection="row"
justifyContent="space-between"
>
<Box minHeight={40} width="75%" pt={2}>
<Typography variant="h4">Cost Overview</Typography>
</Box>
<Box minHeight={40} maxHeight={60} display="flex">
{!!flags.get('cost-insights-currencies') && (
<Box mr={1}>
<CurrencySelect
currency={currency}
currencies={currencies}
onSelect={setCurrency}
/>
</Box>
)}
<ProjectSelect
project={pageFilters.project}
projects={projects || []}
onSelect={onProjectSelect}
/>
</Box>
</Box>
);
return (
<CostInsightsLayout groups={groups}>
<Grid container wrap="nowrap">
@@ -180,15 +224,6 @@ const CostInsightsPage = () => {
justifyContent="flex-end"
mb={2}
>
{!!flags.get('cost-insights-currencies') && (
<Box mr={1}>
<CurrencySelect
currency={currency}
currencies={currencies}
onSelect={setCurrency}
/>
</Box>
)}
<CopyUrlToClipboard />
<CostInsightsSupportButton />
</Box>
@@ -212,6 +247,9 @@ const CostInsightsPage = () => {
<Divider />
</>
)}
<Grid item xs>
<CostOverviewBanner />
</Grid>
<Grid item xs>
<Box px={3} py={6}>
{!!dailyCost.aggregation.length && (
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { MapLoadingToProps } from '../../hooks';
import { getResetState, DefaultLoadingAction } from '../../types';
import {
getResetState,
DefaultLoadingAction,
getResetStateWithoutInitial,
} from '../../types';
type CostInsightsPageLoadingProps = {
loadingActions: Array<string>;
@@ -23,6 +27,7 @@ type CostInsightsPageLoadingProps = {
dispatchInitial: (isLoading: boolean) => void;
dispatchInsights: (isLoading: boolean) => void;
dispatchNone: (loadingActions: string[]) => void;
dispatchReset: (loadingActions: string[]) => void;
};
export const mapLoadingToProps: MapLoadingToProps<CostInsightsPageLoadingProps> = ({
@@ -39,4 +44,6 @@ export const mapLoadingToProps: MapLoadingToProps<CostInsightsPageLoadingProps>
dispatch({ [DefaultLoadingAction.CostInsightsPage]: isLoading }),
dispatchNone: (loadingActions: string[]) =>
dispatch(getResetState(loadingActions)),
dispatchReset: (loadingActions: string[]) =>
dispatch(getResetStateWithoutInitial(loadingActions)),
});
@@ -19,10 +19,8 @@ import { Box, Card, CardContent, Divider } from '@material-ui/core';
import CostOverviewChart from '../CostOverviewChart';
import CostOverviewChartLegend from '../CostOverviewChartLegend';
import CostOverviewHeader from './CostOverviewHeader';
import CostOverviewFooter from './CostOverviewFooter';
import MetricSelect from '../MetricSelect';
import PeriodSelect from '../PeriodSelect';
import ProjectSelect from '../ProjectSelect';
import { useScroll, useFilters, useConfig } from '../../hooks';
import { mapFiltersToProps } from './selector';
import { DefaultNavigation } from '../../utils/navigation';
@@ -45,7 +43,6 @@ const CostOverviewCard = ({
change,
aggregation,
trendline,
projects,
}: CostOverviewCardProps) => {
const { metrics } = useConfig();
const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard);
@@ -73,18 +70,13 @@ const CostOverviewCard = ({
trendline={trendline}
/>
</Box>
<CostOverviewFooter>
<ProjectSelect
project={filters.project}
projects={projects}
onSelect={setProject}
/>
<Box display="flex" justifyContent="flex-end" alignItems="center">
<MetricSelect
metric={metric}
metrics={metrics}
onSelect={setMetric}
/>
</CostOverviewFooter>
</Box>
</CardContent>
</Card>
);
@@ -1,36 +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 { Box } from '@material-ui/core';
type CostOverviewFooterProps = {
children?: React.ReactNode;
};
const CostOverviewFooter = ({ children }: CostOverviewFooterProps) => (
<Box
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center"
>
{React.Children.map(children, child => (
<Box marginY={1}>{child}</Box>
))}
</Box>
);
export default CostOverviewFooter;
@@ -39,7 +39,7 @@ const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
const [resource, setResource] = useState<Maybe<ProductCost>>(null);
const [error, setError] = useState<Maybe<Error>>(null);
const { group, product: productFilter, setProduct } = useFilters(
const { group, product: productFilter, setProduct, project } = useFilters(
mapFiltersToProps(product.kind),
);
const { loadingProduct, dispatchLoading } = useLoading(
@@ -68,6 +68,7 @@ const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
product.kind,
group!,
productFilter!.duration,
project,
);
setResource(p);
} catch (e) {
@@ -87,6 +88,7 @@ const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
productFilter,
group,
product.kind,
project,
]);
const onPeriodSelect = (duration: Duration) => {
@@ -48,7 +48,7 @@ const ProjectSelect = ({ project, projects, onSelect }: ProjectSelectProps) => {
<Select
className={classes.select}
variant="outlined"
value={project}
value={project || 'all'}
renderValue={renderValue}
onChange={handleOnChange}
data-testid="project-filter-select"
@@ -385,6 +385,7 @@ export const useSelectStyles = makeStyles<BackstageTheme>(
select: {
minWidth: 200,
textAlign: 'start',
backgroundColor: theme.palette.background.paper,
},
menuItem: {
minWidth: 200,