From 2da4ef9020a749bd422e6233677c6fb24e92e1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 30 Oct 2022 22:30:16 +0100 Subject: [PATCH 1/7] Make CostOverviewBreakdownChart responsive by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Gomez --- .../CostOverviewCard/CostOverviewBreakdownChart.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx index f6463b65ed..962dc6b332 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx @@ -54,12 +54,14 @@ import { TooltipRenderer } from '../../types/Tooltip'; export type CostOverviewBreakdownChartProps = { costBreakdown: Cost[]; + responsive?: boolean; }; const LOW_COST_THRESHOLD = 0.1; export const CostOverviewBreakdownChart = ({ costBreakdown, + responsive = true, }: CostOverviewBreakdownChartProps) => { const theme = useTheme(); const classes = useStyles(theme); @@ -228,7 +230,7 @@ export const CostOverviewBreakdownChart = ({ /> Date: Sun, 30 Oct 2022 22:30:58 +0100 Subject: [PATCH 2/7] Define a EntityCostInsightsContent extension to show costs per catalog entity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Gomez --- plugins/cost-insights/package.json | 1 + .../cost-insights/src/api/CostInsightsApi.ts | 20 +++ .../CostOverviewCard/CostOverviewCard.tsx | 3 +- .../components/EntityCosts/EntityCosts.tsx | 160 ++++++++++++++++++ .../src/components/EntityCosts/index.ts | 16 ++ plugins/cost-insights/src/example/client.ts | 24 +++ plugins/cost-insights/src/index.ts | 1 + plugins/cost-insights/src/plugin.ts | 14 ++ yarn.lock | 1 + 9 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx create mode 100644 plugins/cost-insights/src/components/EntityCosts/index.ts diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 9896c3e60e..0f53d22457 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -36,6 +36,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-cost-insights-common": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 3c97407f08..ffc2314435 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -24,6 +24,7 @@ import { MetricData, } from '../types'; import { createApiRef } from '@backstage/core-plugin-api'; +import { Entity as CatalogEntity } from '@backstage/catalog-model'; /** @public */ export type ProductInsightsOptions = { @@ -77,6 +78,25 @@ export type CostInsightsApi = { */ getGroupProjects(group: string): Promise; + /** + * Get daily cost aggregations for a given entity and interval time frame. + * + * The return type includes an array of daily cost aggregations as well as statistics about the + * change in cost over the intervals. Calculating these statistics requires us to bucket costs + * into two or more time periods, hence a repeating interval format rather than just a start and + * end date. + * + * The rate of change in this comparison allows teams to reason about their cost growth (or + * reduction) and compare it to metrics important to the business. + * + * Note: implementing this is only required when using the `EntityCostInsightsContent` extension. + * + * @param entity - The catalog entity + * @param intervals - An ISO 8601 repeating interval string, such as R2/P30D/2020-09-01 + * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals + */ + getEntityDailyCost?(entity: CatalogEntity, intervals: string): Promise; + /** * Get daily cost aggregations for a given group and interval time frame. * diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 3e77858967..9eb5f1e190 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -103,7 +103,8 @@ export const CostOverviewCard = ({ }; // Metrics can only be selected on the total cost graph - const showMetricSelect = config.metrics.length && safeTabIndex === 0; + const showMetricSelect = + metricData && config.metrics.length && safeTabIndex === 0; return ( diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx new file mode 100644 index 0000000000..9fdcb9c98b --- /dev/null +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx @@ -0,0 +1,160 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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, { useCallback, useEffect, useState } from 'react'; +import { CostOverviewCard } from '../CostOverviewCard'; +import { + BillingDateProvider, + ConfigProvider, + CurrencyProvider, + FilterProvider, + GroupsProvider, + LoadingProvider, + ScrollProvider, + useFilters, + useLastCompleteBillingDate, + useLoading, +} from '../../hooks'; +import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; +import { Progress, WarningPanel } from '@backstage/core-components'; +import { default as MaterialAlert } from '@material-ui/lab/Alert'; +import { mapLoadingToProps } from '../CostInsightsPage/selector'; +import { intervalsOf } from '../../utils/duration'; +import { costInsightsApiRef } from '../../api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Cost, Maybe } from '@backstage/plugin-cost-insights-common'; + +export const EntityCostsCard = () => { + const client = useApi(costInsightsApiRef); + const { entity } = useEntity(); + + const { + loadingActions, + loadingGroups, + loadingBillingDate, + loadingInitial, + dispatchInitial, + dispatchInsights, + dispatchNone, + } = useLoading(mapLoadingToProps); + + /* eslint-disable react-hooks/exhaustive-deps */ + // The dispatchLoading functions are derived from loading state using mapLoadingToProps, to + // provide nicer props for the component. These are re-derived whenever loading state changes, + // which causes an infinite loop as product panels load and re-trigger the useEffect below. + // Since the functions don't change, we can memoize - but we trigger the same loop if we satisfy + // exhaustive-deps by including the function itself in dependencies. + + const dispatchLoadingInitial = useCallback(dispatchInitial, []); + const dispatchLoadingInsights = useCallback(dispatchInsights, []); + const dispatchLoadingNone = useCallback(dispatchNone, []); + /* eslint-enable react-hooks/exhaustive-deps */ + + const lastCompleteBillingDate = useLastCompleteBillingDate(); + const [dailyCost, setDailyCost] = useState>(null); + const [error, setError] = useState>(null); + const { pageFilters } = useFilters(p => p); + + useEffect(() => { + async function getInsights() { + setError(null); + try { + dispatchLoadingInsights(true); + const intervals = intervalsOf( + pageFilters.duration, + lastCompleteBillingDate, + ); + + const fetchedDailyCost = await client.getEntityDailyCost!( + entity, + intervals, + ); + setDailyCost(fetchedDailyCost); + } catch (e) { + setError(e); + dispatchLoadingNone(loadingActions); + } finally { + dispatchLoadingNone(loadingActions); + dispatchLoadingInitial(false); + dispatchLoadingInsights(false); + } + } + + // Wait for metadata to finish loading + if (!loadingBillingDate) { + getInsights(); + } + }, [ + client, + entity, + pageFilters, + loadingActions, + loadingGroups, + loadingBillingDate, + dispatchLoadingInsights, + dispatchLoadingInitial, + dispatchLoadingNone, + lastCompleteBillingDate, + ]); + + if (loadingInitial) { + return ; + } + + if (error) { + return {error.message}; + } + + if (!dailyCost) { + return No daily costs; + } + + return ; +}; + +export const EntityCosts = () => { + const client = useApi(costInsightsApiRef); + + if (!client.getEntityDailyCost) { + return ( + + ); + } + + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/cost-insights/src/components/EntityCosts/index.ts b/plugins/cost-insights/src/components/EntityCosts/index.ts new file mode 100644 index 0000000000..b8d5106090 --- /dev/null +++ b/plugins/cost-insights/src/components/EntityCosts/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { EntityCosts } from './EntityCosts'; diff --git a/plugins/cost-insights/src/example/client.ts b/plugins/cost-insights/src/example/client.ts index 9c3e20c2b3..a1401a06a2 100644 --- a/plugins/cost-insights/src/example/client.ts +++ b/plugins/cost-insights/src/example/client.ts @@ -38,6 +38,7 @@ import { getGroupedProjects, trendlineOf, } from '../testUtils'; +import { Entity as CatalogEntity } from '@backstage/catalog-model'; /** @public */ export class ExampleCostInsightsClient implements CostInsightsApi { @@ -92,6 +93,29 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return cost; } + async getEntityDailyCost( + entity: CatalogEntity, + intervals: string, + ): Promise { + const aggregation = aggregationFor(intervals, 8_000); + const groupDailyCost: Cost = await this.request( + { entity, intervals }, + { + aggregation: aggregation, + change: changeOf(aggregation), + trendline: trendlineOf(aggregation), + // Optional field providing cost groupings / breakdowns keyed by the type. In this example, + // daily cost grouped by cloud product OR by project / billing account. + groupedCosts: { + product: getGroupedProducts(intervals), + project: getGroupedProjects(intervals), + }, + }, + ); + + return groupDailyCost; + } + async getGroupDailyCost(group: string, intervals: string): Promise { const aggregation = aggregationFor(intervals, 8_000); const groupDailyCost: Cost = await this.request( diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 2c46156005..8a17b1bf43 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -24,6 +24,7 @@ export { costInsightsPlugin, costInsightsPlugin as plugin, CostInsightsPage, + EntityCostInsightsContent, CostInsightsProjectGrowthInstructionsPage, CostInsightsLabelDataflowInstructionsPage, } from './plugin'; diff --git a/plugins/cost-insights/src/plugin.ts b/plugins/cost-insights/src/plugin.ts index 789d803438..04d58d1877 100644 --- a/plugins/cost-insights/src/plugin.ts +++ b/plugins/cost-insights/src/plugin.ts @@ -53,6 +53,20 @@ export const CostInsightsPage = costInsightsPlugin.provide( }), ); +/** + * An extension for displaying costs on an entity page. + * + * @public + */ +export const EntityCostInsightsContent = costInsightsPlugin.provide( + createRoutableExtension({ + name: 'EntityCostInsightsContent', + component: () => + import('./components/EntityCosts').then(m => m.EntityCosts), + mountPoint: rootRouteRef, + }), +); + /** @public */ export const CostInsightsProjectGrowthInstructionsPage = costInsightsPlugin.provide( diff --git a/yarn.lock b/yarn.lock index 6a88a61621..74b78b52d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5470,6 +5470,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-cost-insights-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" From 6b2933a0a134f1b880faa411f6ec0e0d519d77dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 30 Oct 2022 22:31:09 +0100 Subject: [PATCH 3/7] Update demo app to show costs for service components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Gomez --- packages/app/src/components/catalog/EntityPage.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 8e72bb03e4..02b00f038c 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -153,6 +153,7 @@ import { TextSize, ReportIssue, } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { EntityCostInsightsContent } from '@backstage/plugin-cost-insights'; const customEntityFilterKind = ['Component', 'API', 'System']; @@ -489,6 +490,10 @@ const serviceEntityPage = ( + + + + Date: Sun, 30 Oct 2022 22:48:31 +0100 Subject: [PATCH 4/7] Add a simple test showing that it doesn't explode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Gomez --- .../EntityCosts/EntityCost.test.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx new file mode 100644 index 0000000000..395480c6df --- /dev/null +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { + changeOf, + MockAggregatedDailyCosts, + MockBillingDateProvider, + MockConfigProvider, + MockFilterProvider, + MockScrollProvider, + trendlineOf, +} from '../../testUtils'; +import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; +import { EntityCostsCard } from './EntityCosts'; +import { TestApiProvider } from '@backstage/test-utils'; +import { CostInsightsApi, costInsightsApiRef } from '../../api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; +import { LoadingProvider } from '../../hooks'; +import { Cost } from '@backstage/plugin-cost-insights-common'; + +function renderInContext(children: JSX.Element) { + const mockEntity = { + metadata: { name: 'mock' }, + kind: 'MockKind', + } as Entity; + const mockGroupDailyCost: Cost = { + id: 'test-group', + aggregation: MockAggregatedDailyCosts, + change: changeOf(MockAggregatedDailyCosts), + trendline: trendlineOf(MockAggregatedDailyCosts), + }; + const mockApi: jest.Mocked = { + getLastCompleteBillingDate: jest.fn().mockResolvedValue('2022-10-30'), + getUserGroups: jest.fn().mockResolvedValue(['team-a']), + getGroupProjects: jest.fn().mockResolvedValue(['project-a', 'project-b']), + getEntityDailyCost: jest.fn().mockResolvedValue(mockGroupDailyCost), + getGroupDailyCost: jest.fn().mockResolvedValue({}), + getProjectDailyCost: jest.fn().mockResolvedValue({}), + getDailyMetricData: jest.fn().mockResolvedValue({}), + getProductInsights: jest.fn().mockResolvedValue({}), + getAlerts: jest.fn().mockResolvedValue({}), + }; + + return renderInTestApp( + + + + + + + + {children} + + + + + + + , + ); +} + +describe('', () => { + beforeEach(() => { + // @ts-expect-error: Since we have strictNullChecks enabled, this will throw an error as window.ResizeObserver + // it's not an optional operand + delete window.ResizeObserver; + window.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + })); + }); + + afterEach(() => { + window.ResizeObserver = ResizeObserver; + jest.restoreAllMocks(); + }); + + it('Renders without exploding', async () => { + const { getByText } = await renderInContext(); + expect(getByText('Cloud Cost')).toBeInTheDocument(); + }); +}); From bc4353ca19ef075f0439ee311b48bf43ae9cf1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Tue, 8 Nov 2022 23:22:13 +0100 Subject: [PATCH 5/7] Use reference instead of plain entities in the CostInsightsApi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Gomez --- plugins/cost-insights/api-report.md | 12 ++++++++++++ plugins/cost-insights/src/api/CostInsightsApi.ts | 11 +++++++---- .../src/components/EntityCosts/EntityCost.test.tsx | 2 +- .../src/components/EntityCosts/EntityCosts.tsx | 9 +++++---- plugins/cost-insights/src/example/client.ts | 7 +++---- .../src/example/templates/CostInsightsClient.ts | 11 +++++++++++ 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md index f50f0b0c81..dd4e404450 100644 --- a/plugins/cost-insights/api-report.md +++ b/plugins/cost-insights/api-report.md @@ -267,6 +267,10 @@ export type CostInsightsApi = { getLastCompleteBillingDate(): Promise; getUserGroups(userId: string): Promise; getGroupProjects(group: string): Promise; + getCatalogEntityDailyCost?( + catalogEntityRef: string, + intervals: string, + ): Promise; getGroupDailyCost(group: string, intervals: string): Promise; getProjectDailyCost(project: string, intervals: string): Promise; getDailyMetricData(metric: string, intervals: string): Promise; @@ -404,11 +408,19 @@ export const EngineerThreshold = 0.5; // @public @deprecated (undocumented) export type Entity = common.Entity; +// @public +export const EntityCostInsightsContent: () => JSX.Element; + // @public (undocumented) export class ExampleCostInsightsClient implements CostInsightsApi { // (undocumented) getAlerts(group: string): Promise; // (undocumented) + getCatalogEntityDailyCost( + entityRef: string, + intervals: string, + ): Promise; + // (undocumented) getDailyMetricData(metric: string, intervals: string): Promise; // (undocumented) getGroupDailyCost(group: string, intervals: string): Promise; diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index ffc2314435..a7065a04aa 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -24,7 +24,6 @@ import { MetricData, } from '../types'; import { createApiRef } from '@backstage/core-plugin-api'; -import { Entity as CatalogEntity } from '@backstage/catalog-model'; /** @public */ export type ProductInsightsOptions = { @@ -79,7 +78,7 @@ export type CostInsightsApi = { getGroupProjects(group: string): Promise; /** - * Get daily cost aggregations for a given entity and interval time frame. + * Get daily cost aggregations for a given catalog entity and interval time frame. * * The return type includes an array of daily cost aggregations as well as statistics about the * change in cost over the intervals. Calculating these statistics requires us to bucket costs @@ -91,11 +90,15 @@ export type CostInsightsApi = { * * Note: implementing this is only required when using the `EntityCostInsightsContent` extension. * - * @param entity - The catalog entity + * @param catalogEntityRef - A reference to the catalog entity, as described in + * https://backstage.io/docs/features/software-catalog/references * @param intervals - An ISO 8601 repeating interval string, such as R2/P30D/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ - getEntityDailyCost?(entity: CatalogEntity, intervals: string): Promise; + getCatalogEntityDailyCost?( + catalogEntityRef: string, + intervals: string, + ): Promise; /** * Get daily cost aggregations for a given group and interval time frame. diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx index 395480c6df..53e1a13ebf 100644 --- a/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx @@ -48,7 +48,7 @@ function renderInContext(children: JSX.Element) { getLastCompleteBillingDate: jest.fn().mockResolvedValue('2022-10-30'), getUserGroups: jest.fn().mockResolvedValue(['team-a']), getGroupProjects: jest.fn().mockResolvedValue(['project-a', 'project-b']), - getEntityDailyCost: jest.fn().mockResolvedValue(mockGroupDailyCost), + getCatalogEntityDailyCost: jest.fn().mockResolvedValue(mockGroupDailyCost), getGroupDailyCost: jest.fn().mockResolvedValue({}), getProjectDailyCost: jest.fn().mockResolvedValue({}), getDailyMetricData: jest.fn().mockResolvedValue({}), diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx index 9fdcb9c98b..9dda1c65e3 100644 --- a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx @@ -37,6 +37,7 @@ import { costInsightsApiRef } from '../../api'; import { useApi } from '@backstage/core-plugin-api'; import { useEntity } from '@backstage/plugin-catalog-react'; import { Cost, Maybe } from '@backstage/plugin-cost-insights-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; export const EntityCostsCard = () => { const client = useApi(costInsightsApiRef); @@ -79,8 +80,8 @@ export const EntityCostsCard = () => { lastCompleteBillingDate, ); - const fetchedDailyCost = await client.getEntityDailyCost!( - entity, + const fetchedDailyCost = await client.getCatalogEntityDailyCost!( + stringifyEntityRef(entity), intervals, ); setDailyCost(fetchedDailyCost); @@ -129,11 +130,11 @@ export const EntityCostsCard = () => { export const EntityCosts = () => { const client = useApi(costInsightsApiRef); - if (!client.getEntityDailyCost) { + if (!client.getCatalogEntityDailyCost) { return ( ); } diff --git a/plugins/cost-insights/src/example/client.ts b/plugins/cost-insights/src/example/client.ts index a1401a06a2..e2a183b682 100644 --- a/plugins/cost-insights/src/example/client.ts +++ b/plugins/cost-insights/src/example/client.ts @@ -38,7 +38,6 @@ import { getGroupedProjects, trendlineOf, } from '../testUtils'; -import { Entity as CatalogEntity } from '@backstage/catalog-model'; /** @public */ export class ExampleCostInsightsClient implements CostInsightsApi { @@ -93,13 +92,13 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return cost; } - async getEntityDailyCost( - entity: CatalogEntity, + async getCatalogEntityDailyCost( + entityRef: string, intervals: string, ): Promise { const aggregation = aggregationFor(intervals, 8_000); const groupDailyCost: Cost = await this.request( - { entity, intervals }, + { entityRef, intervals }, { aggregation: aggregation, change: changeOf(aggregation), diff --git a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts index fc68eb62c7..50c3bb1470 100644 --- a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts +++ b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts @@ -87,6 +87,17 @@ export class CostInsightsClient implements CostInsightsApi { } } + async getCatalogEntityDailyCost(catalogEntityRef: string, intervals: string): Promise { + return { + id: 'remove-me', + aggregation: [], + change: { + ratio: 0, + amount: 0 + } + } + } + async getProductInsights(options: ProductInsightsOptions): Promise { return { id: 'remove-me', From a446ce237e836fc5b8d44cacb3953ee68a75857f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Tue, 8 Nov 2022 23:36:05 +0100 Subject: [PATCH 6/7] Add EntityCostInsightsContent to the dev mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Gomez --- plugins/cost-insights/dev/index.tsx | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/plugins/cost-insights/dev/index.tsx b/plugins/cost-insights/dev/index.tsx index 4be788427a..622abaffc9 100644 --- a/plugins/cost-insights/dev/index.tsx +++ b/plugins/cost-insights/dev/index.tsx @@ -23,7 +23,26 @@ import { CostInsightsPage, CostInsightsProjectGrowthInstructionsPage, CostInsightsLabelDataflowInstructionsPage, + EntityCostInsightsContent, } from '../src/plugin'; +import { Content, Header, Page } from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; + +const mockEntity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + description: 'backstage.io', + }, + spec: { + lifecycle: 'production', + type: 'service', + owner: 'user:guest', + }, +}; createDevApp() .registerPlugin(costInsightsPlugin) @@ -44,4 +63,22 @@ createDevApp() title: 'Labelling', element: , }) + .addPage({ + title: 'Entity', + element: ( + +
+ + + + + + + + + + + + ), + }) .render(); From 745e0e2228991729556ff76fe0285f11ce014dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Tue, 8 Nov 2022 23:39:39 +0100 Subject: [PATCH 7/7] Add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Gomez --- .changeset/shaggy-moles-jump.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaggy-moles-jump.md diff --git a/.changeset/shaggy-moles-jump.md b/.changeset/shaggy-moles-jump.md new file mode 100644 index 0000000000..b2417beb9e --- /dev/null +++ b/.changeset/shaggy-moles-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Added support for displaying entity cost insights by implementing the new `getCatalogEntityDailyCost` that's part of the `CostInsightsApi`.