diff --git a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts index 083a0e2ace..a109c3b2eb 100644 --- a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts +++ b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts @@ -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, ): Promise { + 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 { diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index b7628e2f9e..bdf673b7d1 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -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, ): Promise; /** diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index c426eff8bc..8922ac31e0 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -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>(null); const [error, setError] = useState>(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) => + setPageFilters({ + ...pageFilters, + project: project === 'all' ? null : project, + }); + useEffect(() => { async function getInsights() { setError(null); @@ -165,6 +174,41 @@ const CostInsightsPage = () => { ); } + const onProjectSelect = (project: Maybe) => { + setProject(project); + dispatchLoadingReset(loadingActions); + }; + + const CostOverviewBanner = () => ( + + + Cost Overview + + + {!!flags.get('cost-insights-currencies') && ( + + + + )} + + + + ); + return ( @@ -180,15 +224,6 @@ const CostInsightsPage = () => { justifyContent="flex-end" mb={2} > - {!!flags.get('cost-insights-currencies') && ( - - - - )} @@ -212,6 +247,9 @@ const CostInsightsPage = () => { )} + + + {!!dailyCost.aggregation.length && ( diff --git a/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx b/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx index 51db47c082..8a9cf021c9 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/selector.tsx @@ -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; @@ -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 = ({ @@ -39,4 +44,6 @@ export const mapLoadingToProps: MapLoadingToProps dispatch({ [DefaultLoadingAction.CostInsightsPage]: isLoading }), dispatchNone: (loadingActions: string[]) => dispatch(getResetState(loadingActions)), + dispatchReset: (loadingActions: string[]) => + dispatch(getResetStateWithoutInitial(loadingActions)), }); diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index ef174132a3..1d9543c8c0 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -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} /> - - + - + ); diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewFooter.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewFooter.tsx deleted file mode 100644 index 03920887b8..0000000000 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewFooter.tsx +++ /dev/null @@ -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) => ( - - {React.Children.map(children, child => ( - {child} - ))} - -); - -export default CostOverviewFooter; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index 1d24139c64..c9790a5bfa 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -39,7 +39,7 @@ const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => { const [resource, setResource] = useState>(null); const [error, setError] = useState>(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) => { diff --git a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx index cbcd8ede4e..934ad99f63 100644 --- a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx +++ b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx @@ -48,7 +48,7 @@ const ProjectSelect = ({ project, projects, onSelect }: ProjectSelectProps) => {