From f20ea86c615ee2d85fa21076b9eb208e5ae96ccb Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 3 Apr 2025 14:37:07 +0200 Subject: [PATCH] feat: added network traffic bundle (#9691) --- .../NetworkTrafficUsage.tsx | 26 ++++--- .../NetworkTrafficUsage/RequestSummary.tsx | 21 +++++ .../hooks/useChartDataSelection.ts | 2 +- .../hooks/useTrafficLimit.test.ts | 77 ------------------- .../hooks/useTrafficLimit.ts | 24 ------ .../useTrafficBundles/useTrafficBundles.ts | 76 ++++++++++++++++++ frontend/src/interfaces/trafficBundles.ts | 4 + 7 files changed, 119 insertions(+), 111 deletions(-) delete mode 100644 frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.test.ts delete mode 100644 frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.ts create mode 100644 frontend/src/hooks/api/getters/useTrafficBundles/useTrafficBundles.ts create mode 100644 frontend/src/interfaces/trafficBundles.ts diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 428f246ec8..96d9e01d02 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -18,7 +18,6 @@ import { import { Bar } from 'react-chartjs-2'; import annotationPlugin from 'chartjs-plugin-annotation'; import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin'; -import { useTrafficLimit } from './hooks/useTrafficLimit'; import { PeriodSelector } from './PeriodSelector'; import { useUiFlag } from 'hooks/useUiFlag'; import { OverageInfo, RequestSummary } from './RequestSummary'; @@ -27,6 +26,7 @@ import { getChartLabel } from './chart-functions'; import { useTrafficStats } from './hooks/useStats'; import { BoldText, StyledBox, TopRow } from './SharedComponents'; import { useChartDataSelection } from './hooks/useChartDataSelection'; +import { useTrafficBundles } from '../../../../hooks/api/getters/useTrafficBundles/useTrafficBundles'; const TrafficInfoBoxes = styled('div')(({ theme }) => ({ display: 'grid', @@ -42,10 +42,13 @@ const NetworkTrafficUsage: FC = () => { const { isOss } = useUiConfig(); - const includedTraffic = useTrafficLimit(); + const { trafficBundles } = useTrafficBundles(); + + const totalTraffic = + trafficBundles.includedTraffic + trafficBundles.purchasedTraffic; const { chartDataSelection, setChartDataSelection, options } = - useChartDataSelection(includedTraffic); + useChartDataSelection(totalTraffic); const { chartData, @@ -53,19 +56,19 @@ const NetworkTrafficUsage: FC = () => { overageCost, estimatedMonthlyCost, requestSummaryUsage, - } = useTrafficStats(includedTraffic, chartDataSelection); + } = useTrafficStats(totalTraffic, chartDataSelection); const showOverageCalculations = chartDataSelection.grouping === 'daily' && chartDataSelection.month === currentMonth && - includedTraffic > 0 && - usageTotal - includedTraffic > 0 && + totalTraffic > 0 && + usageTotal - totalTraffic > 0 && estimateTrafficDataCost; const showConsumptionBillingWarning = (chartDataSelection.grouping === 'monthly' || chartDataSelection.month === currentMonth) && - includedTraffic > 0 && + totalTraffic > 0 && overageCost > 0; return ( @@ -100,12 +103,17 @@ const NetworkTrafficUsage: FC = () => { {showOverageCalculations && ( ({ @@ -70,6 +71,7 @@ export const RequestSummary: FC = ({ period, usageTotal, includedTraffic, + purchasedTraffic, }) => { const { locationSettings } = useLocationSettings(); @@ -104,6 +106,25 @@ export const RequestSummary: FC = ({ )} + {purchasedTraffic > 0 && ( + +
Additional traffic purchased
+
+ {purchasedTraffic.toLocaleString('en-US')} requests +
+
+ )} + {includedTraffic > 0 && ( + +
Total traffic
+
+ {( + includedTraffic + purchasedTraffic + ).toLocaleString('en-US')}{' '} + requests +
+
+ )} ); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts index 6c317c8639..2cb12bb0b9 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts @@ -51,7 +51,7 @@ export const useChartDataSelection = (includedTraffic?: number) => { }, includedTraffic, ); - }, [theme, chartDataSelection]); + }, [theme, chartDataSelection, includedTraffic]); return { chartDataSelection, setChartDataSelection, options }; }; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.test.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.test.ts deleted file mode 100644 index f9dacbe66c..0000000000 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { useTrafficLimit } from './useTrafficLimit'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { useUiFlag } from 'hooks/useUiFlag'; -import { vi, describe, it, expect } from 'vitest'; - -vi.mock('hooks/api/getters/useUiConfig/useUiConfig'); -vi.mock('hooks/useUiFlag'); - -describe('useTrafficLimit', () => { - it('should return requests limit if user is on pro plan', () => { - vi.mocked(useUiConfig).mockReturnValue({ - isPro: () => true, - isEnterprise: () => false, - uiConfig: { - billing: 'pay-as-you-go', - }, - } as any); - vi.mocked(useUiFlag).mockReturnValue(false); - - const { result } = renderHook(() => useTrafficLimit()); - - expect(result.current).toBe(53_000_000); - }); - - it('should return PAYG plan requests limit if enterprise-payg is enabled and billing is pay-as-you-go', () => { - vi.mocked(useUiConfig).mockReturnValue({ - isPro: () => false, - isEnterprise: () => true, - uiConfig: { billing: 'pay-as-you-go' }, - } as any); - vi.mocked(useUiFlag).mockReturnValue(true); - - const { result } = renderHook(() => useTrafficLimit()); - - expect(result.current).toBe(53_000_000); - }); - - it('should return 0 if user is not on pro plan and pay-as-you-go conditions are not met', () => { - vi.mocked(useUiConfig).mockReturnValue({ - isPro: () => false, - isEnterprise: () => false, - uiConfig: {}, - } as any); - vi.mocked(useUiFlag).mockReturnValue(false); - - const { result } = renderHook(() => useTrafficLimit()); - - expect(result.current).toBe(0); - }); - - it('should return 0 if user is not on pro plan and flag is disabled', () => { - vi.mocked(useUiConfig).mockReturnValue({ - isPro: () => false, - isEnterprise: () => true, - uiConfig: { billing: 'pay-as-you-go' }, - } as any); - vi.mocked(useUiFlag).mockReturnValue(false); - - const { result } = renderHook(() => useTrafficLimit()); - - expect(result.current).toBe(0); - }); - - it('should return 0 if enterprise PAYG is enabled but billing is not pay-as-you-go', () => { - vi.mocked(useUiConfig).mockReturnValue({ - isPro: () => false, - isEnterprise: () => false, - uiConfig: { billing: 'subscription' }, - } as any); - vi.mocked(useUiFlag).mockReturnValue(true); - - const { result } = renderHook(() => useTrafficLimit()); - - expect(result.current).toBe(0); - }); -}); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.ts deleted file mode 100644 index 688b78b7dd..0000000000 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useTrafficLimit.ts +++ /dev/null @@ -1,24 +0,0 @@ -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { useUiFlag } from 'hooks/useUiFlag'; - -const proPlanIncludedRequests = 53_000_000; -const paygPlanIncludedRequests = proPlanIncludedRequests; - -export const useTrafficLimit = () => { - const { isPro, isEnterprise, uiConfig } = useUiConfig(); - const isEnterprisePaygEnabled = useUiFlag('enterprise-payg'); - - if (isPro()) { - return proPlanIncludedRequests; - } - - if ( - isEnterprisePaygEnabled && - isEnterprise() && - uiConfig.billing === 'pay-as-you-go' - ) { - return paygPlanIncludedRequests; - } - - return 0; -}; diff --git a/frontend/src/hooks/api/getters/useTrafficBundles/useTrafficBundles.ts b/frontend/src/hooks/api/getters/useTrafficBundles/useTrafficBundles.ts new file mode 100644 index 0000000000..55ffe89208 --- /dev/null +++ b/frontend/src/hooks/api/getters/useTrafficBundles/useTrafficBundles.ts @@ -0,0 +1,76 @@ +import { formatApiPath } from '../../../../utils/formatPath'; +import type { ITrafficBundles } from '../../../../interfaces/trafficBundles'; +import useSWR from 'swr'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import { useMemo } from 'react'; +import useUiConfig from '../useUiConfig/useUiConfig'; + +type IUseTrafficBundlesOutput = { + trafficBundles: ITrafficBundles; + loading: boolean; + error?: Error; + refetch: () => void; +}; + +const DEFAULT_TRAFFIC_INCLUDED_PAYG = 53; +const DEFAULT_TRAFFIC_INCLUDED_ENTERPRISE = 259; +const DEFAULT_TRAFFIC_MULTIPLIER = 1_000_000; + +const defaultIncludedTraffic = ( + isEnterprise: boolean, + billing?: string, +): number => { + if (!isEnterprise || billing === 'pay-as-you-go') { + return DEFAULT_TRAFFIC_INCLUDED_PAYG; + } + return DEFAULT_TRAFFIC_INCLUDED_ENTERPRISE; +}; + +function includedTraffic( + includedTraffic: number | undefined, + isEnterprise: boolean, + billing?: string, +): number { + if (includedTraffic === undefined) { + return defaultIncludedTraffic(isEnterprise, billing); + } + return includedTraffic; +} + +function purchasedTraffic(purchasedTraffic: number | undefined): number { + if (purchasedTraffic === undefined) { + return 0; + } + return purchasedTraffic; +} + +export const useTrafficBundles = (): IUseTrafficBundlesOutput => { + const { isEnterprise, uiConfig } = useUiConfig(); + const path = formatApiPath('/api/instance/trafficBundles'); + const { data, error, mutate } = useSWR(path, fetcher); + + const trafficBundles = useMemo(() => { + return { + includedTraffic: + includedTraffic( + data?.includedTraffic, + isEnterprise(), + uiConfig.billing, + ) * DEFAULT_TRAFFIC_MULTIPLIER, + purchasedTraffic: + purchasedTraffic(data?.purchasedTraffic) * + DEFAULT_TRAFFIC_MULTIPLIER, + }; + }, [data]); + return { + trafficBundles, + loading: !error && !data, + refetch: mutate, + }; +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('configuration')) + .then((res) => res.json()); +}; diff --git a/frontend/src/interfaces/trafficBundles.ts b/frontend/src/interfaces/trafficBundles.ts new file mode 100644 index 0000000000..e5bb235db6 --- /dev/null +++ b/frontend/src/interfaces/trafficBundles.ts @@ -0,0 +1,4 @@ +export interface ITrafficBundles { + includedTraffic: number; + purchasedTraffic: number; +}