diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index 96c813919c11..ac65ed379b97 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -23,6 +23,7 @@ import { LicenseBanner } from './banners/internalBanners/LicenseBanner'; import { Demo } from './demo/Demo'; import { LoginRedirect } from './common/LoginRedirect/LoginRedirect'; import { SecurityBanner } from './banners/internalBanners/SecurityBanner'; +import { TrafficOverageBanner } from './banners/TrafficOverageBanner'; const StyledContainer = styled('div')(() => ({ '& ul': { @@ -68,6 +69,7 @@ export const App = () => { /> + diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 3c6ab14fb920..7beb26d7f8ef 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -239,20 +239,11 @@ export const NetworkTrafficUsage: VFC = () => { elseShow={ <> 0 && overageCost > 0} + condition={includedTraffic > 0} show={ - - Heads up! You are currently consuming - more requests than your plan includes and will - be billed according to our terms. Please see{' '} - - this page - {' '} - for more information. In order to reduce your - traffic consumption, you may configure an{' '} + + In order to reduce your traffic consumption, + consider setting up an{' '} { + const period = toSelectablePeriod(new Date()).key; + + testServerRoute(server, '/api/admin/ui-config', { + environment: 'Enterprise', + versionInfo: { + current: { + enterprise: 'Enterprise', + }, + }, + billing: 'pay-as-you-go', + flags: { + 'enterprise-payg': true, + estimateTrafficDataCost: true, + }, + }); + + testServerRoute(server, `/api/admin/metrics/traffic/${period}`, { + period, + apiData: [ + { + apiPath: '/api/client', + days: [ + { + day: `${period}-01T00:00:00.000Z`, + trafficTypes: [ + { + group: 'successful-requests', + count: totalRequests, + }, + ], + }, + ], + }, + ], + }); +}; + +test('Displays overage banner when overage cost is calculated', async () => { + setupApi(BILLING_INCLUDED_REQUESTS + 1_000_000); + + render(); + + const bannerMessage = await screen.findByText( + /You're using more requests than your plan/, + ); + expect(bannerMessage).toBeInTheDocument(); +}); + +test('Displays estimated monthly cost banner when usage is projected to exceed', async () => { + setupApi(BILLING_INCLUDED_REQUESTS - 1_000_000); + + render(); + + const bannerMessage = await screen.findByText( + /Based on your current usage, you're projected to exceed your plan/, + ); + expect(bannerMessage).toBeInTheDocument(); +}); + +test('Does not display banner when no overage or estimated cost', async () => { + setupApi(); + + render(); + + expect(screen.queryByText('Heads up!')).not.toBeInTheDocument(); +}); diff --git a/frontend/src/component/banners/TrafficOverageBanner.tsx b/frontend/src/component/banners/TrafficOverageBanner.tsx new file mode 100644 index 000000000000..e31bb2046d43 --- /dev/null +++ b/frontend/src/component/banners/TrafficOverageBanner.tsx @@ -0,0 +1,84 @@ +import { useUiFlag } from 'hooks/useUiFlag'; +import { Banner } from './Banner/Banner'; +import { useTrafficDataEstimation } from 'hooks/useTrafficData'; +import { useTrafficLimit } from 'component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit'; +import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import { BILLING_TRAFFIC_BUNDLE_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan'; +import { useMemo } from 'react'; + +export const TrafficOverageBanner = () => { + const estimateTrafficDataCostEnabled = useUiFlag('estimateTrafficDataCost'); + const includedTraffic = useTrafficLimit(); + const { + currentPeriod, + toChartData, + toTrafficUsageSum, + endpointsInfo, + getDayLabels, + calculateOverageCost, + calculateEstimatedMonthlyCost, + } = useTrafficDataEstimation(); + const traffic = useInstanceTrafficMetrics(currentPeriod.key); + + const trafficData = useMemo( + () => + toChartData( + getDayLabels(currentPeriod.dayCount), + traffic, + endpointsInfo, + ), + [traffic, currentPeriod, endpointsInfo], + ); + + const calculatedOverageCost = useMemo( + () => + includedTraffic + ? calculateOverageCost( + toTrafficUsageSum(trafficData), + includedTraffic, + BILLING_TRAFFIC_BUNDLE_PRICE, + ) + : 0, + [includedTraffic, trafficData], + ); + + const estimatedMonthlyCost = useMemo( + () => + includedTraffic && estimateTrafficDataCostEnabled + ? calculateEstimatedMonthlyCost( + currentPeriod.key, + trafficData, + includedTraffic, + new Date(), + BILLING_TRAFFIC_BUNDLE_PRICE, + ) + : 0, + [ + includedTraffic, + estimateTrafficDataCostEnabled, + trafficData, + currentPeriod, + ], + ); + + const overageMessage = + calculatedOverageCost > 0 + ? `**Heads up!** You're using more requests than your plan [includes](https://www.getunleash.io/pricing), and additional charges will apply per our [terms](https://www.getunleash.io/fair-use-policy).` + : estimatedMonthlyCost > 0 + ? `**Heads up!** Based on your current usage, you're projected to exceed your plan's [limit](https://www.getunleash.io/pricing), and additional charges may apply per our [terms](https://www.getunleash.io/fair-use-policy).` + : undefined; + + if (!overageMessage) return null; + + return ( + + ); +}; diff --git a/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts b/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts index b95d82d68c34..aa81b12be3fe 100644 --- a/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts +++ b/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts @@ -1,8 +1,14 @@ -import useSWR from 'swr'; import { useMemo } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import type { TrafficUsageDataSegmentedSchema } from 'openapi'; +import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; +import useUiConfig from '../useUiConfig/useUiConfig'; + +const DEFAULT_DATA: TrafficUsageDataSegmentedSchema = { + apiData: [], + period: '', +}; export interface IInstanceTrafficMetricsResponse { usage: TrafficUsageDataSegmentedSchema; @@ -17,14 +23,21 @@ export interface IInstanceTrafficMetricsResponse { export const useInstanceTrafficMetrics = ( period: string, ): IInstanceTrafficMetricsResponse => { - const { data, error, mutate } = useSWR( - formatApiPath(`api/admin/metrics/traffic/${period}`), - fetcher, - ); + const { + isPro, + uiConfig: { billing }, + } = useUiConfig(); + const { data, error, mutate } = + useConditionalSWR( + isPro() || billing === 'pay-as-you-go', + DEFAULT_DATA, + formatApiPath(`api/admin/metrics/traffic/${period}`), + fetcher, + ); return useMemo( () => ({ - usage: data, + usage: data ?? DEFAULT_DATA, loading: !error && !data, refetch: () => mutate(), error,