diff --git a/.changeset/cost-insights-hot-cheetahs-shout.md b/.changeset/cost-insights-hot-cheetahs-shout.md new file mode 100644 index 0000000000..3d9fa31378 --- /dev/null +++ b/.changeset/cost-insights-hot-cheetahs-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Add support for non-SKU breakdowns for entities in the product panels. diff --git a/app-config.yaml b/app-config.yaml index 3b476981e4..54d564de7d 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -290,6 +290,9 @@ costInsights: bigQuery: name: BigQuery icon: search + events: + name: Events + icon: data metrics: DAU: name: Daily Active Users diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx index c01ab67ecc..d617c0df35 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx @@ -29,7 +29,7 @@ function createRenderer(col: keyof RowData, classes: Record) { const row = rowData as RowData; const rowStyles = classnames(classes.row, { [classes.rowTotal]: row.id === 'total', - [classes.colFirst]: col === 'sku', + [classes.colFirst]: col === 'label', [classes.colLast]: col === 'ratio', }); @@ -50,7 +50,7 @@ function createRenderer(col: keyof RowData, classes: Record) { /> ); default: - return {row.sku}; + return {row.label}; } }; } @@ -63,7 +63,7 @@ function createSorter(field?: keyof Omit) { const b = data2 as RowData; if (a.id === 'total') return 1; if (b.id === 'total') return 1; - if (field === 'sku') return a.sku.localeCompare(b.sku); + if (field === 'label') return a.label.localeCompare(b.label); return field ? a[field] - b[field] @@ -80,7 +80,7 @@ const defaultEntity: Entity = { type RowData = { id: string; - sku: string; + label: string; previous: number; current: number; ratio: number; @@ -93,6 +93,7 @@ type ProductEntityDialogOptions = Partial< type ProductEntityDialogProps = { open: boolean; entity?: Entity; + entitiesLabel: string; options?: ProductEntityDialogOptions; onClose: () => void; }; @@ -100,6 +101,7 @@ type ProductEntityDialogProps = { export const ProductEntityDialog = ({ open, entity = defaultEntity, + entitiesLabel, options = {}, onClose, }: ProductEntityDialogProps) => { @@ -118,10 +120,12 @@ export const ProductEntityDialog = ({ const columns: TableColumn[] = [ { - field: 'sku', - title: SKU, - render: createRenderer('sku', classes), - customSort: createSorter('sku'), + field: 'label', + title: ( + {entitiesLabel} + ), + render: createRenderer('label', classes), + customSort: createSorter('label'), width: '33.33%', }, { @@ -154,14 +158,14 @@ export const ProductEntityDialog = ({ const rowData: RowData[] = entity.entities .map(e => ({ id: e.id || 'Unknown', - sku: e.id || 'Unknown', + label: e.id || 'Unknown', previous: e.aggregation[0], current: e.aggregation[1], ratio: e.change.ratio, })) .concat({ id: 'total', - sku: 'Total', + label: 'Total', previous: entity.aggregation[0], current: entity.aggregation[1], ratio: entity.change.ratio, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx index 221a99708b..2c26f62574 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -66,9 +66,9 @@ export const ProductInsightsChart = ({ const [selectLabel, setSelected] = useState>(); const isSelected = useMemo(() => !isUndefined(selectLabel), [selectLabel]); const isClickable = useMemo(() => { - const skus = + const breakdownEntities = entity.entities.find(e => e.id === activeLabel)?.entities ?? []; - return skus.length > 0; + return breakdownEntities.length > 0; }, [entity, activeLabel]); const legendTitle = `Cost ${entity.change.ratio <= 0 ? 'Savings' : 'Growth'}`; @@ -122,10 +122,13 @@ export const ProductInsightsChart = ({ const activeEntity = findAlways(entity.entities, e => e.id === id); const ratio = activeEntity.change.ratio; - const skus = activeEntity.entities; - const subtitle = `${skus.length} ${pluralOf(skus.length, 'SKU')}`; + const breakdownEntities = activeEntity.entities; + const subtitle = `${breakdownEntities.length} ${pluralOf( + breakdownEntities.length, + entity.entitiesLabel || 'SKU', + )}`; - if (skus.length) { + if (breakdownEntities.length) { return ( setSelected(undefined)} entity={entity.entities.find(e => e.id === selectLabel)} options={options} + entitiesLabel={entity.entitiesLabel || 'SKU'} /> )} diff --git a/plugins/cost-insights/src/types/Entity.ts b/plugins/cost-insights/src/types/Entity.ts index 83347f05c3..52f6271bb4 100644 --- a/plugins/cost-insights/src/types/Entity.ts +++ b/plugins/cost-insights/src/types/Entity.ts @@ -22,6 +22,7 @@ export interface Entity { aggregation: [number, number]; entities: Entity[]; change: ChangeStatistic; + entitiesLabel?: string; } /* diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 98eab19b41..76bdf5f08f 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -812,7 +812,7 @@ export const SampleComputeEngineInsights: Entity = { id: 'Sample SKU A', aggregation: [1_000, 2_000], change: { - ratio: 0.5, + ratio: 1, amount: 1_000, }, entities: [], @@ -849,6 +849,92 @@ export const SampleComputeEngineInsights: Entity = { ], }; +export const SampleEventsInsights: Entity = { + id: 'events', + aggregation: [20_000, 10_000], + change: { + ratio: -0.5, + amount: -10_000, + }, + entitiesLabel: 'Product', + entities: [ + { + id: 'entity-a', + aggregation: [15_000, 7_000], + change: { + ratio: -0.53333333333, + amount: -8_000, + }, + entities: [ + { + id: 'Sample Product A', + aggregation: [5_000, 2_000], + change: { + ratio: -0.6, + amount: -3_000, + }, + entities: [], + }, + { + id: 'Sample Product B', + aggregation: [7_000, 2_500], + change: { + ratio: -0.64285714285, + amount: -4_500, + }, + entities: [], + }, + { + id: 'Sample Product C', + aggregation: [3_000, 2_500], + change: { + ratio: -0.16666666666, + amount: -500, + }, + entities: [], + }, + ], + }, + { + id: 'entity-b', + aggregation: [5_000, 3_000], + change: { + ratio: -0.4, + amount: -2_000, + }, + entities: [ + { + id: 'Sample Product A', + aggregation: [2_000, 1_000], + change: { + ratio: -0.5, + amount: -1_000, + }, + entities: [], + }, + { + id: 'Sample Product B', + aggregation: [1_000, 1_500], + change: { + ratio: 0.5, + amount: 500, + }, + entities: [], + }, + { + id: 'Sample Product C', + aggregation: [2_000, 500], + change: { + ratio: -0.75, + amount: -1_500, + }, + entities: [], + }, + ], + }, + ], +}; + export function entityOf(product: string): Entity { switch (product) { case 'computeEngine': @@ -859,6 +945,8 @@ export function entityOf(product: string): Entity { return SampleCloudStorageInsights; case 'bigQuery': return SampleBigQueryInsights; + case 'events': + return SampleEventsInsights; default: throw new Error( `Cannot get insights for ${product}. Make sure product matches product property in app-info.yaml`,