Skip to content

Commit 3913303

Browse files
authored
Support multi-provider FinOps benchmark banner on HomePage (#2169)
Fixes OPS-3991.
1 parent 06a2070 commit 3913303

6 files changed

Lines changed: 124 additions & 48 deletions

File tree

packages/react-ui/src/app/features/home/components/home-onboarding-view.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
FlowTemplateMetadataWithIntegrations,
1919
NoWorkflowsPlaceholder,
2020
} from '@openops/components/ui';
21-
import { Permission } from '@openops/shared';
21+
import { BenchmarkProviders, Permission } from '@openops/shared';
2222
import { t } from 'i18next';
2323
import { useEffect, useState } from 'react';
2424
import { useNavigate } from 'react-router-dom';
@@ -105,17 +105,17 @@ const HomeOnboardingView = ({
105105
const {
106106
isEnabled: isFinOpsBenchmarkEnabled,
107107
variation: benchmarkVariation,
108-
provider: benchmarkProvider,
108+
providers: benchmarkProviders,
109109
} = useBenchmarkBannerState();
110-
const onViewBenchmarkReportClick = () =>
111-
navigate(`/analytics?dashboard=${benchmarkProvider}_benchmark`);
110+
const onViewBenchmarkReportClick = (provider: BenchmarkProviders) =>
111+
navigate(`/analytics?dashboard=${provider}_benchmark`);
112112

113113
return (
114114
<div className="flex flex-col gap-6 flex-1">
115115
{isFinOpsBenchmarkEnabled && (
116116
<FinOpsBenchmarkBanner
117117
variation={benchmarkVariation}
118-
provider={benchmarkProvider}
118+
providers={benchmarkProviders}
119119
onActionClick={openBenchmarkWizard}
120120
onViewReportClick={onViewBenchmarkReportClick}
121121
disabled={!hasBenchmarkPermissions}

packages/react-ui/src/app/features/home/components/home-operational-view.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from '@/app/features/home/lib/home-hooks';
77
import { HomeRunsTable } from '@/app/features/home/runs-table';
88
import { FinOpsBenchmarkBanner, OverviewCard } from '@openops/components/ui';
9-
import { Permission } from '@openops/shared';
9+
import { BenchmarkProviders, Permission } from '@openops/shared';
1010
import { subDays } from 'date-fns';
1111
import { t } from 'i18next';
1212
import {
@@ -47,16 +47,16 @@ const HomeOperationalView = ({
4747
const {
4848
isEnabled: isFinOpsBenchmarkEnabled,
4949
variation: benchmarkVariation,
50-
provider: benchmarkProvider,
50+
providers: benchmarkProviders,
5151
} = useBenchmarkBannerState();
5252
const { checkAccess } = useAuthorization();
5353
const hasBenchmarkPermissions =
5454
checkAccess(Permission.WRITE_FLOW) &&
5555
checkAccess(Permission.READ_APP_CONNECTION);
5656

5757
const openBenchmarkWizard = useOpenBenchmarkWizard();
58-
const onViewBenchmarkReportClick = () =>
59-
navigate(`/analytics?dashboard=${benchmarkProvider}_benchmark`);
58+
const onViewBenchmarkReportClick = (provider: BenchmarkProviders) =>
59+
navigate(`/analytics?dashboard=${provider}_benchmark`);
6060

6161
return (
6262
<>
@@ -110,7 +110,7 @@ const HomeOperationalView = ({
110110
{isFinOpsBenchmarkEnabled && (
111111
<FinOpsBenchmarkBanner
112112
variation={benchmarkVariation}
113-
provider={benchmarkProvider}
113+
providers={benchmarkProviders}
114114
onActionClick={openBenchmarkWizard}
115115
onViewReportClick={onViewBenchmarkReportClick}
116116
disabled={!hasBenchmarkPermissions}

packages/react-ui/src/app/features/home/components/useBenchmarkBannerState.test.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('useBenchmarkBannerState', () => {
4949

5050
expect(result.current.isEnabled).toBe(false);
5151
expect(result.current.variation).toBe('default');
52-
expect(result.current.provider).toBe(undefined);
52+
expect(result.current.providers).toEqual([]);
5353
});
5454

5555
it('does not call listBenchmarks when banner should not be shown', () => {
@@ -91,7 +91,7 @@ describe('useBenchmarkBannerState', () => {
9191

9292
expect(result.current.isEnabled).toBe(true);
9393
expect(result.current.variation).toBe('default');
94-
expect(result.current.provider).toBe(undefined);
94+
expect(result.current.providers).toEqual([]);
9595
});
9696

9797
it('returns default variation when banner is shown but benchmark is RUNNING', () => {
@@ -152,7 +152,7 @@ describe('useBenchmarkBannerState', () => {
152152
const { result } = renderHook(() => useBenchmarkBannerState());
153153

154154
expect(result.current.variation).toBe('report');
155-
expect(result.current.provider).toBe('aws');
155+
expect(result.current.providers).toEqual(['aws']);
156156
});
157157

158158
it('returns report variation when there are multiple benchmarks and one has SUCCEEDED', () => {
@@ -172,6 +172,59 @@ describe('useBenchmarkBannerState', () => {
172172
const { result } = renderHook(() => useBenchmarkBannerState());
173173

174174
expect(result.current.variation).toBe('report');
175-
expect(result.current.provider).toBe('azure');
175+
expect(result.current.providers).toEqual(['azure']);
176+
});
177+
178+
it('returns succeeded providers in API order', () => {
179+
mockUseShowBenchmarkBanner.mockReturnValue({
180+
showBanner: true,
181+
isPending: false,
182+
});
183+
setupQueryMock([
184+
{
185+
benchmarkId: 'bm-1',
186+
provider: 'azure',
187+
status: BenchmarkStatus.SUCCEEDED,
188+
},
189+
{
190+
benchmarkId: 'bm-2',
191+
provider: 'aws',
192+
status: BenchmarkStatus.SUCCEEDED,
193+
},
194+
]);
195+
196+
const { result } = renderHook(() => useBenchmarkBannerState());
197+
198+
expect(result.current.variation).toBe('report');
199+
expect(result.current.providers).toEqual(['azure', 'aws']);
200+
});
201+
202+
it('dedupes providers when multiple succeeded benchmarks share provider', () => {
203+
mockUseShowBenchmarkBanner.mockReturnValue({
204+
showBanner: true,
205+
isPending: false,
206+
});
207+
setupQueryMock([
208+
{
209+
benchmarkId: 'bm-1',
210+
provider: 'aws',
211+
status: BenchmarkStatus.SUCCEEDED,
212+
},
213+
{
214+
benchmarkId: 'bm-2',
215+
provider: 'aws',
216+
status: BenchmarkStatus.SUCCEEDED,
217+
},
218+
{
219+
benchmarkId: 'bm-3',
220+
provider: 'azure',
221+
status: BenchmarkStatus.SUCCEEDED,
222+
},
223+
]);
224+
225+
const { result } = renderHook(() => useBenchmarkBannerState());
226+
227+
expect(result.current.variation).toBe('report');
228+
expect(result.current.providers).toEqual(['aws', 'azure']);
176229
});
177230
});

packages/react-ui/src/app/features/home/components/useBenchmarkBannerState.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useShowBenchmarkBanner } from './useShowBenchmarkBanner';
77
type BenchmarkBannerState = {
88
isEnabled: boolean;
99
variation: 'default' | 'report';
10-
provider?: BenchmarkProviders;
10+
providers: BenchmarkProviders[];
1111
};
1212

1313
export const useBenchmarkBannerState = (): BenchmarkBannerState => {
@@ -19,13 +19,22 @@ export const useBenchmarkBannerState = (): BenchmarkBannerState => {
1919
enabled: isEnabled,
2020
});
2121

22-
const succeededBenchmark = benchmarks?.find(
23-
(b) => b.status === BenchmarkStatus.SUCCEEDED,
24-
);
22+
const succeededProviders =
23+
benchmarks?.reduce<BenchmarkProviders[]>((providers, benchmark) => {
24+
if (benchmark.status !== BenchmarkStatus.SUCCEEDED) {
25+
return providers;
26+
}
27+
28+
if (!providers.includes(benchmark.provider)) {
29+
providers.push(benchmark.provider);
30+
}
31+
32+
return providers;
33+
}, []) ?? [];
2534

2635
return {
2736
isEnabled,
28-
variation: succeededBenchmark ? 'report' : 'default',
29-
provider: succeededBenchmark?.provider,
37+
variation: succeededProviders.length > 0 ? 'report' : 'default',
38+
providers: succeededProviders,
3039
};
3140
};

packages/ui-components/src/components/finops-benchmark-banner/finops-benchmark-banner.tsx

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BenchmarkProviders } from '@openops/shared';
12
import { cva } from 'class-variance-authority';
23
import { t } from 'i18next';
34
import { LucideBarChart2, Sparkles } from 'lucide-react';
@@ -21,40 +22,42 @@ const finOpsActionsVariants = cva('flex shrink-0 items-center', {
2122
},
2223
});
2324

24-
const PROVIDER_LABELS: Record<string, string> = {
25-
aws: 'AWS',
26-
azure: 'Azure',
25+
const PROVIDER_LABELS: Record<BenchmarkProviders, string> = {
26+
[BenchmarkProviders.AWS]: 'AWS',
27+
[BenchmarkProviders.AZURE]: 'Azure',
2728
};
2829

2930
type FinOpsBenchmarkBannerProps = {
3031
variation?: 'default' | 'report';
31-
provider?: 'aws' | 'azure';
32+
providers?: BenchmarkProviders[];
3233
onActionClick?: () => void;
33-
onViewReportClick?: () => void;
34+
onViewReportClick?: (provider: BenchmarkProviders) => void;
3435
className?: string;
3536
disabled?: boolean;
3637
};
3738

3839
const FinOpsBenchmarkBanner = ({
3940
variation = 'default',
40-
provider = 'aws',
41+
providers = [BenchmarkProviders.AWS],
4142
onActionClick,
4243
onViewReportClick,
4344
className,
4445
disabled = false,
4546
}: FinOpsBenchmarkBannerProps) => {
4647
const permissionMessage = usePermissionMessage();
4748
const disabledTooltip = disabled ? permissionMessage : undefined;
48-
const providerLabel = PROVIDER_LABELS[provider];
4949

5050
const content =
5151
variation === 'report'
5252
? {
5353
title: t('FinOps Benchmark'),
5454
description:
55-
t('Your') + ` ${providerLabel} ` + t('benchmark report is ready.'),
55+
providers.length > 1
56+
? t('Your benchmark reports are ready.')
57+
: t('Your') +
58+
` ${PROVIDER_LABELS[providers[0]]} ` +
59+
t('benchmark report is ready.'),
5660
actionLabel: t('Re-run a Benchmark'),
57-
reportLabel: t('See') + ` ${providerLabel} ` + t('Benchmark Report'),
5861
}
5962
: {
6063
title: t('FinOps Benchmark'),
@@ -75,22 +78,25 @@ const FinOpsBenchmarkBanner = ({
7578
</p>
7679
</div>
7780
<div className={finOpsActionsVariants({ variation })}>
78-
{variation === 'report' && (
79-
<TooltipWrapper tooltipText={disabledTooltip}>
80-
<span className={cn({ 'cursor-not-allowed': disabled })}>
81-
<Button
82-
type="button"
83-
variant="ghost"
84-
disabled={disabled}
85-
className="h-auto gap-1 px-0 py-0 text-sm font-bold leading-5 text-primary-200 hover:bg-transparent hover:underline disabled:pointer-events-none disabled:opacity-50"
86-
onClick={onViewReportClick}
87-
>
88-
<LucideBarChart2 className="size-[18px]" />
89-
{content.reportLabel}
90-
</Button>
91-
</span>
92-
</TooltipWrapper>
93-
)}
81+
{variation === 'report' &&
82+
providers.map((provider) => (
83+
<TooltipWrapper key={provider} tooltipText={disabledTooltip}>
84+
<span className={cn({ 'cursor-not-allowed': disabled })}>
85+
<Button
86+
type="button"
87+
variant="ghost"
88+
disabled={disabled}
89+
className="h-auto gap-1 px-0 py-0 text-sm font-bold leading-5 text-primary-200 hover:bg-transparent hover:underline disabled:pointer-events-none disabled:opacity-50"
90+
onClick={() => onViewReportClick?.(provider)}
91+
>
92+
<LucideBarChart2 className="size-[18px]" />
93+
{t('See') +
94+
` ${PROVIDER_LABELS[provider]} ` +
95+
t('Benchmark Report')}
96+
</Button>
97+
</span>
98+
</TooltipWrapper>
99+
))}
94100
<TooltipWrapper tooltipText={disabledTooltip}>
95101
<span className={cn({ 'cursor-not-allowed': disabled })}>
96102
<Button

packages/ui-components/src/stories/finops-benchmark-banner/finops-benchmark-banner.stories.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BenchmarkProviders } from '@openops/shared';
12
import { action } from '@storybook/addon-actions';
23
import type { Meta, StoryObj } from '@storybook/react';
34
import { ThemeAwareDecorator } from '../../../.storybook/decorators';
@@ -31,14 +32,21 @@ export const Default: Story = {};
3132
export const AwsReport: Story = {
3233
args: {
3334
variation: 'report',
34-
provider: 'aws',
35+
providers: [BenchmarkProviders.AWS],
3536
},
3637
};
3738

3839
export const AzureReport: Story = {
3940
args: {
4041
variation: 'report',
41-
provider: 'azure',
42+
providers: [BenchmarkProviders.AZURE],
43+
},
44+
};
45+
46+
export const MultiProviderReport: Story = {
47+
args: {
48+
variation: 'report',
49+
providers: [BenchmarkProviders.AWS, BenchmarkProviders.AZURE],
4250
},
4351
};
4452

@@ -51,7 +59,7 @@ export const DisabledDefault: Story = {
5159
export const DisabledReport: Story = {
5260
args: {
5361
variation: 'report',
54-
provider: 'aws',
62+
providers: [BenchmarkProviders.AWS],
5563
disabled: true,
5664
},
5765
};

0 commit comments

Comments
 (0)