Merge pull request #2789 from spotify/update-project-select
Add global project filter to Cost Insights overview page
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user