diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx index e1ecfd71dc..96b6fe75bf 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react'; +import { useState, useEffect } from 'react'; import { Alert, Divider, Grid, styled, Typography } from '@mui/material'; import { Link } from 'react-router-dom'; import CheckIcon from '@mui/icons-material/Check'; @@ -15,6 +16,9 @@ import { GridRow } from 'component/common/GridRow/GridRow'; import { GridCol } from 'component/common/GridCol/GridCol'; import { Badge } from 'component/common/Badge/Badge'; import { GridColLink } from './GridColLink/GridColLink'; +import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import { useTrafficDataEstimation } from 'hooks/useTrafficData'; +import { useUiFlag } from 'hooks/useUiFlag'; const StyledPlanBox = styled('aside')(({ theme }) => ({ padding: theme.spacing(2.5), @@ -71,10 +75,21 @@ interface IBillingPlanProps { instanceStatus: IInstanceStatus; } +const proPlanIncludedRequests = 53_000_000; + export const BillingPlan: FC = ({ instanceStatus }) => { const { users } = useUsers(); const expired = trialHasExpired(instanceStatus); - const { uiConfig } = useUiConfig(); + const { uiConfig, isPro } = useUiConfig(); + + const { + currentPeriod, + toChartData, + toTrafficUsageSum, + endpointsInfo, + getDayLabels, + calculateOverageCost, + } = useTrafficDataEstimation(); const eligibleUsers = users.filter((user: any) => user.email); @@ -94,6 +109,32 @@ export const BillingPlan: FC = ({ instanceStatus }) => { const paidAssignedPrice = price.user * paidAssigned; const finalPrice = planPrice + paidAssignedPrice; const inactive = instanceStatus.state !== InstanceState.ACTIVE; + const [totalCost, setTotalCost] = useState(0); + + const flagEnabled = useUiFlag('displayTrafficDataUsage'); + const [overageCost, setOverageCost] = useState(0); + + const includedTraffic = isPro() ? proPlanIncludedRequests : 0; + const traffic = useInstanceTrafficMetrics(currentPeriod.key); + + useEffect(() => { + if (flagEnabled && includedTraffic > 0) { + const trafficData = toChartData( + getDayLabels(currentPeriod.dayCount), + traffic, + endpointsInfo, + ); + const totalTraffic = toTrafficUsageSum(trafficData); + const overageCostCalc = calculateOverageCost( + totalTraffic, + includedTraffic, + ); + setOverageCost(overageCostCalc); + setTotalCost(finalPrice + overageCostCalc); + } else { + setTotalCost(finalPrice); + } + }, [traffic]); return ( @@ -185,7 +226,11 @@ export const BillingPlan: FC = ({ instanceStatus }) => { - + ({ + marginBottom: theme.spacing(1.5), + })} + > Paid members @@ -210,6 +255,40 @@ export const BillingPlan: FC = ({ instanceStatus }) => { + 0} + show={ + + + + + Accrued traffic charges + + + + view details + + + + + $5 dollar per 1 million + started above included data + + + + ({ + fontSize: + theme.fontSizes + .mainHeader, + })} + > + ${overageCost.toFixed(2)} + + + + } + /> @@ -223,7 +302,7 @@ export const BillingPlan: FC = ({ instanceStatus }) => { theme.fontSizes.mainHeader, })} > - Total per month + Total @@ -234,7 +313,7 @@ export const BillingPlan: FC = ({ instanceStatus }) => { fontSize: '2rem', })} > - ${finalPrice.toFixed(2)} + ${totalCost.toFixed(2)} diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 8af2fbca8d..506ce8a321 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -4,7 +4,8 @@ import styled from '@mui/material/styles/styled'; import { usePageTitle } from 'hooks/usePageTitle'; import Select from 'component/common/select'; import Box from '@mui/system/Box'; -import Alert from '@mui/material/Alert'; +import { Link as RouterLink } from 'react-router-dom'; +import { Alert, Link } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { @@ -13,7 +14,6 @@ import { CategoryScale, LinearScale, BarElement, - type ChartDataset, Title, Tooltip, Legend, @@ -22,136 +22,22 @@ import { } from 'chart.js'; import { Bar } from 'react-chartjs-2'; -import { - type IInstanceTrafficMetricsResponse, - useInstanceTrafficMetrics, -} from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import type { Theme } from '@mui/material/styles/createTheme'; import Grid from '@mui/material/Grid'; import { useUiFlag } from 'hooks/useUiFlag'; import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary'; import annotationPlugin from 'chartjs-plugin-annotation'; - -type ChartDatasetType = ChartDataset<'bar'>; - -type SelectablePeriod = { - key: string; - dayCount: number; - label: string; - year: number; - month: number; -}; - -type EndpointInfo = { - label: string; - color: string; - order: number; -}; +import { + type ChartDatasetType, + useTrafficDataEstimation, +} from 'hooks/useTrafficData'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'grid', gap: theme.spacing(5), })); -const padMonth = (month: number): string => - month < 10 ? `0${month}` : `${month}`; - -const toSelectablePeriod = (date: Date, label?: string): SelectablePeriod => { - const year = date.getFullYear(); - const month = date.getMonth(); - const period = `${year}-${padMonth(month + 1)}`; - const dayCount = new Date(year, month + 1, 0).getDate(); - return { - key: period, - year, - month, - dayCount, - label: - label || - date.toLocaleString('en-US', { month: 'long', year: 'numeric' }), - }; -}; - -const getSelectablePeriods = (): SelectablePeriod[] => { - const current = new Date(Date.now()); - const selectablePeriods = [toSelectablePeriod(current, 'Current month')]; - for ( - let subtractMonthCount = 1; - subtractMonthCount < 13; - subtractMonthCount++ - ) { - // JavaScript wraps around the year, so we don't need to handle that. - const date = new Date( - current.getFullYear(), - current.getMonth() - subtractMonthCount, - 1, - ); - if (date > new Date('2024-03-31')) { - selectablePeriods.push(toSelectablePeriod(date)); - } - } - return selectablePeriods; -}; - -const toPeriodsRecord = ( - periods: SelectablePeriod[], -): Record => { - return periods.reduce( - (acc, period) => { - acc[period.key] = period; - return acc; - }, - {} as Record, - ); -}; - -const getDayLabels = (dayCount: number): number[] => { - return [...Array(dayCount).keys()].map((i) => i + 1); -}; - -const toChartData = ( - days: number[], - traffic: IInstanceTrafficMetricsResponse, - endpointsInfo: Record, -): ChartDatasetType[] => { - if (!traffic || !traffic.usage || !traffic.usage.apiData) { - return []; - } - - const data = traffic.usage.apiData - .filter((item) => !!endpointsInfo[item.apiPath]) - .sort( - (item1: any, item2: any) => - endpointsInfo[item1.apiPath].order - - endpointsInfo[item2.apiPath].order, - ) - .map((item: any) => { - const daysRec = days.reduce( - (acc, day: number) => { - acc[`d${day}`] = 0; - return acc; - }, - {} as Record, - ); - - for (const dayKey in item.days) { - const day = item.days[dayKey]; - const dayNum = new Date(Date.parse(day.day)).getDate(); - daysRec[`d${dayNum}`] = day.trafficTypes[0].count; - } - const epInfo = endpointsInfo[item.apiPath]; - - return { - label: epInfo.label, - data: Object.values(daysRec), - backgroundColor: epInfo.color, - hoverBackgroundColor: epInfo.color, - }; - }); - - return data; -}; - const customHighlightPlugin = { id: 'customLine', beforeDraw: (chart: Chart) => { @@ -295,36 +181,27 @@ const createBarChartOptions = ( }, }); -const endpointsInfo: Record = { - '/api/admin': { - label: 'Admin', - color: '#6D66D9', - order: 1, - }, - '/api/frontend': { - label: 'Frontend', - color: '#A39EFF', - order: 2, - }, - '/api/client': { - label: 'Server', - color: '#D8D6FF', - order: 3, - }, -}; - const proPlanIncludedRequests = 53_000_000; export const NetworkTrafficUsage: VFC = () => { usePageTitle('Network - Data Usage'); const theme = useTheme(); - const selectablePeriods = getSelectablePeriods(); - const record = toPeriodsRecord(selectablePeriods); - const [period, setPeriod] = useState(selectablePeriods[0].key); - const { isOss, isPro } = useUiConfig(); + const { + record, + period, + setPeriod, + selectablePeriods, + getDayLabels, + toChartData, + toTrafficUsageSum, + endpointsInfo, + calculateOverageCost, + calculateEstimatedMonthlyCost, + } = useTrafficDataEstimation(); + const includedTraffic = isPro() ? proPlanIncludedRequests : 0; const options = useMemo(() => { @@ -355,6 +232,10 @@ export const NetworkTrafficUsage: VFC = () => { const [usageTotal, setUsageTotal] = useState(0); + const [overageCost, setOverageCost] = useState(0); + + const [estimatedMonthlyCost, setEstimatedMonthlyCost] = useState(0); + const data = { labels, datasets, @@ -375,20 +256,21 @@ export const NetworkTrafficUsage: VFC = () => { useEffect(() => { if (data) { - const usage = data.datasets.reduce( - (acc: number, current: ChartDatasetType) => { - return ( - acc + - current.data.reduce( - (acc_inner, current_inner) => - acc_inner + current_inner, - 0, - ) - ); - }, - 0, + const usage = toTrafficUsageSum(data.datasets); + const calculatedOverageCost = calculateOverageCost( + usage, + includedTraffic, ); setUsageTotal(usage); + setOverageCost(calculatedOverageCost); + setEstimatedMonthlyCost( + calculateEstimatedMonthlyCost( + period, + data.datasets, + includedTraffic, + new Date(), + ), + ); } }, [data]); @@ -398,15 +280,40 @@ export const NetworkTrafficUsage: VFC = () => { show={Not enabled.} elseShow={ <> + 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{' '} + + Unleash Edge instance + {' '} + in your own datacenter. + + } + /> - - - +