From e72a7c1197f88e00fb18fba16a5d96a5cac9d93e Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 31 Jan 2025 14:05:36 +0100 Subject: [PATCH] chore(1-3316): update request info boxes to new design (#9180) Updates the existing number of requests and overage info boxes to the new design. The existing versions of the boxes had some issues on narrower screens, so it was easier to just leave them as is and start from scratch. The previous boxes on narrow screens: ![image](https://github.com/user-attachments/assets/f3efa00d-ac0d-41ed-82d8-11766e043cb5) The current ones (from wide to narrower): Wide ![image](https://github.com/user-attachments/assets/0a48c013-afcd-4652-9229-0fca19a83733) Mid (the text should probably ideally wrap at the same time here, but I'm not sure how at the moment) ![image](https://github.com/user-attachments/assets/2ea3a672-80a6-4445-ae90-736c91c6e88e) Narrow ![image](https://github.com/user-attachments/assets/03e3de0e-23c1-436a-8f6c-4c78cd4fdae7) Extra narrow: ![image](https://github.com/user-attachments/assets/652c0c3b-71b1-4b2e-9e86-217f0c827aa6) There's still some work we **could** do, but we should have UX have a look first. In particular, it's about how the text wraps in certain places etc, but I think it's good enough for now. I'll come back with tests for the calculations and some refactoring and cleanup in a followup. --- .../NetworkTrafficUsage.tsx | 111 +++++++++---- .../NetworkTrafficUsage/RequestSummary.tsx | 156 ++++++++++++++++++ .../average-traffic-previous-months.ts | 34 ++++ frontend/src/component/common/Badge/Badge.tsx | 4 +- frontend/src/hooks/useTrafficData.ts | 4 +- 5 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx create mode 100644 frontend/src/component/admin/network/NetworkTrafficUsage/average-traffic-previous-months.ts 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 };