diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 041f9b9eab..db47da7845 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -42,6 +42,8 @@ import { PeriodSelector } from './PeriodSelector'; import { useUiFlag } from 'hooks/useUiFlag'; import { format } from 'date-fns'; import { monthlyTrafficDataToCurrentUsage } from './monthly-traffic-data-to-current-usage'; +import { OverageInfo, RequestSummary } from './RequestSummary'; +import { averageTrafficPreviousMonths } from './average-traffic-previous-months'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'grid', @@ -148,16 +150,30 @@ const createBarChartOptions = ( }); // this is primarily for dev purposes. The existing grid is very inflexible, so we might want to change it, but for demoing the design, this is enough. -const NewHeader = styled('div')(() => ({ +const NewHeader = styled('div')(({ theme }) => ({ display: 'flex', + flexFlow: 'row wrap', justifyContent: 'space-between', - alignItems: 'flex-start', + gap: theme.spacing(2, 4), +})); + +const TrafficInfoBoxes = styled('div')(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(200px, max-content))', + flex: 1, + gap: theme.spacing(2, 4), +})); + +const BoldText = styled('span')(({ theme }) => ({ + fontWeight: 'bold', })); const NewNetworkTrafficUsage: FC = () => { usePageTitle('Network - Data Usage'); const theme = useTheme(); + const estimateTrafficDataCost = useUiFlag('estimateTrafficDataCost'); + const { isOss } = useUiConfig(); const { locationSettings } = useLocationSettings(); @@ -168,6 +184,7 @@ const NewNetworkTrafficUsage: FC = () => { toTrafficUsageSum, calculateOverageCost, calculateEstimatedMonthlyCost, + endpointsInfo, } = useTrafficDataEstimation(); const includedTraffic = useTrafficLimit(); @@ -192,7 +209,11 @@ const NewNetworkTrafficUsage: FC = () => { }, ); } else { - return new Date(tooltipItems[0].label).toLocaleDateString( + const timestamp = Date.parse(tooltipItems[0].label); + if (Number.isNaN(timestamp)) { + return 'Current month to date'; + } + return new Date(timestamp).toLocaleDateString( locationSettings?.locale ?? 'en-US', { month: 'long', @@ -248,6 +269,21 @@ const NewNetworkTrafficUsage: FC = () => { } }, [data]); + // todo: extract this (and also endpoints info) + const [lastLabel, ...otherLabels] = Object.values(endpointsInfo) + .map((info) => info.label.toLowerCase()) + .toReversed(); + const requestTypes = `${otherLabels.toReversed().join(', ')}, and ${lastLabel}`; + const chartLabel = + newPeriod.grouping === 'daily' + ? `A bar chart showing daily traffic usage for ${new Date( + newPeriod.month, + ).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + })}. Each date shows ${requestTypes} requests.` + : `A bar chart showing monthly total traffic usage for the current month and the preceding ${newPeriod.monthsBack} months. Each month shows ${requestTypes} requests.`; + return ( { elseShow={ <> 0 && overageCost > 0} + condition={ + (newPeriod.grouping === 'monthly' || + newPeriod.month === + format(new Date(), 'yyyy-MM')) && + includedTraffic > 0 && + overageCost > 0 + } show={ - Heads up! You are currently consuming - more requests than your plan includes and will - be billed according to our terms. Please see{' '} - + 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. } /> - { - // todo: add new usage plan summary that works for monthly data as well as daily - } - - + + + {newPeriod.grouping === 'daily' && + includedTraffic > 0 && + usageTotal - includedTraffic > 0 && + estimateTrafficDataCost && ( + + )} + { data={data} plugins={[customHighlightPlugin()]} options={options} - aria-label='An instance metrics line chart with two lines: requests per second for admin API and requests per second for client API' // todo: this isn't correct at all! + aria-label={chartLabel} /> diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx new file mode 100644 index 0000000000..da6531dea0 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx @@ -0,0 +1,156 @@ +import { Link, styled } from '@mui/material'; +import { Badge } from 'component/common/Badge/Badge'; +import { subMonths } from 'date-fns'; +import type { ChartDataSelection } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import type { FC } from 'react'; + +type Props = { + period: ChartDataSelection; + usageTotal: number; + includedTraffic: number; +}; + +const Container = styled('article')(({ theme }) => ({ + minWidth: '200px', + border: `2px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadiusLarge, + padding: theme.spacing(3), + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), +})); + +const Header = styled('h3')(({ theme }) => ({ + margin: 0, + fontSize: theme.typography.body1.fontSize, +})); + +const List = styled('dl')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2.5), + padding: 0, + margin: 0, +})); + +const Row = styled('div')(({ theme }) => ({ + display: 'flex', + flexFlow: 'row wrap', + justifyContent: 'space-between', + gap: theme.spacing(1, 3), + fontSize: theme.typography.body2.fontSize, + color: theme.palette.text.secondary, + + '& dd': { + margin: 0, + color: theme.palette.text.primary, + }, +})); + +const incomingRequestsText = (period: ChartDataSelection): string => { + const formatMonth = (date: Date) => + date.toLocaleString('en-US', { + month: 'short', + year: 'numeric', + }); + + if (period.grouping === 'monthly') { + const currentMonth = new Date(); + const fromMonth = subMonths(currentMonth, period.monthsBack); + const toMonth = subMonths(currentMonth, 1); + return `Average requests from ${formatMonth(fromMonth)} to ${formatMonth(toMonth)}`; + } + + return `Incoming requests in ${formatMonth(new Date(period.month))}`; +}; + +export const RequestSummary: FC = ({ + period, + usageTotal, + includedTraffic, +}) => { + const { locationSettings } = useLocationSettings(); + + return ( + +
Number of requests to Unleash
+ + +
{incomingRequestsText(period)}
+
+ 0 + ? usageTotal <= includedTraffic + ? 'success' + : 'error' + : 'neutral' + } + tabIndex={-1} + > + {usageTotal.toLocaleString( + locationSettings.locale ?? 'en-US', + )}{' '} + requests + +
+
+ {includedTraffic > 0 && ( + +
Included in your plan monthly
+
+ {includedTraffic.toLocaleString('en-US')} requests +
+
+ )} +
+
+ ); +}; + +type OverageProps = { + overages: number; + overageCost: number; + estimatedMonthlyCost: number; +}; + +export const OverageInfo: FC = ({ + overages, + overageCost, + estimatedMonthlyCost, +}) => { + return ( + +
Accrued traffic charges
+ + +
+ Request overages this month ( + + pricing + + ) +
+
{overages.toLocaleString()} requests
+
+ +
Accrued traffic charges
+
+ {overageCost} USD +
+
+ {estimatedMonthlyCost > 0 && ( + +
Estimated charges based on current usage
+
+ + {estimatedMonthlyCost} USD + +
+
+ )} +
+
+ ); +}; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/average-traffic-previous-months.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/average-traffic-previous-months.ts new file mode 100644 index 0000000000..c274251547 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/average-traffic-previous-months.ts @@ -0,0 +1,34 @@ +import { differenceInCalendarMonths, format } from 'date-fns'; +import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi'; + +export const averageTrafficPreviousMonths = ( + endpointData: string[], + traffic: TrafficUsageDataSegmentedCombinedSchema, +) => { + if (!traffic || traffic.grouping === 'daily') { + return 0; + } + + const monthsToCount = Math.abs( + differenceInCalendarMonths( + new Date(traffic.dateRange.to), + new Date(traffic.dateRange.from), + ), + ); + + const currentMonth = format(new Date(), 'yyyy-MM'); + + const totalTraffic = traffic.apiData + .filter((endpoint) => endpointData.includes(endpoint.apiPath)) + .map((endpoint) => + endpoint.dataPoints + .filter(({ period }) => period !== currentMonth) + .reduce( + (acc, current) => acc + current.trafficTypes[0].count, + 0, + ), + ) + .reduce((total, next) => total + next, 0); + + return Math.round(totalTraffic / monthsToCount); +}; diff --git a/frontend/src/component/common/Badge/Badge.tsx b/frontend/src/component/common/Badge/Badge.tsx index d6a0c9889f..e1a29ab8ff 100644 --- a/frontend/src/component/common/Badge/Badge.tsx +++ b/frontend/src/component/common/Badge/Badge.tsx @@ -29,6 +29,7 @@ interface IBadgeProps { children?: ReactNode; title?: string; onClick?: (event: React.SyntheticEvent) => void; + tabIndex?: number; } interface IBadgeIconProps { @@ -96,13 +97,14 @@ export const Badge: FC = forwardRef( className, sx, children, + tabIndex = 0, ...props }: IBadgeProps, ref: ForwardedRef, ) => ( - formatMonth(addMonths(from, index)), + index === numMonths - 1 + ? 'Current month' + : formatMonth(addMonths(from, index)), ); return { datasets, labels };