diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx index 30cce5032f..54afb5024d 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx @@ -15,6 +15,7 @@ import { useTrafficDataEstimation } from 'hooks/useTrafficData'; import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import { useMemo } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { calculateOverageCost } from 'utils/traffic-calculations'; const StyledInfoLabel = styled(Typography)(({ theme }) => ({ fontSize: theme.fontSizes.smallBody, @@ -39,7 +40,6 @@ export const BillingDetailsPAYG = ({ toTrafficUsageSum, endpointsInfo, getDayLabels, - calculateOverageCost, } = useTrafficDataEstimation(); const eligibleUsers = users.filter((user) => user.email); diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx index 08c8467690..d4eb3b5e67 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx @@ -17,6 +17,7 @@ import { BILLING_TRAFFIC_BUNDLE_PRICE, } from './BillingPlan'; import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import { calculateOverageCost } from 'utils/traffic-calculations'; const StyledInfoLabel = styled(Typography)(({ theme }) => ({ fontSize: theme.fontSizes.smallBody, @@ -47,7 +48,6 @@ export const BillingDetailsPro = ({ toTrafficUsageSum, endpointsInfo, getDayLabels, - calculateOverageCost, } = useTrafficDataEstimation(); const eligibleUsers = users.filter((user) => user.email); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 0e10cbd4da..d0a3522b48 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -28,11 +28,7 @@ import type { Theme } from '@mui/material/styles/createTheme'; import Grid from '@mui/material/Grid'; import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary'; import annotationPlugin from 'chartjs-plugin-annotation'; -import { - type ChartDatasetType, - newToChartData, - useTrafficDataEstimation, -} from 'hooks/useTrafficData'; +import { useTrafficDataEstimation } from 'hooks/useTrafficData'; import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin'; import { formatTickValue } from 'component/common/Chart/formatTickValue'; import { useTrafficLimit } from './hooks/useTrafficLimit'; @@ -40,10 +36,22 @@ import { BILLING_TRAFFIC_BUNDLE_PRICE } from 'component/admin/billing/BillingDas import { useLocationSettings } from 'hooks/useLocationSettings'; 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'; +import { + calculateTotalUsage, + calculateOverageCost, + calculateEstimatedMonthlyCost, +} from 'utils/traffic-calculations'; +import { currentDate, currentMonth } from './dates'; +import { endpointsInfo } from './endpoint-info'; +import { type ChartDataSelection, toDateRange } from './chart-data-selection'; +import { + type ChartDatasetType, + getChartLabel, + toChartData as newToChartData, +} from './chart-functions'; +import { periodsRecord, selectablePeriods } from './selectable-periods'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'grid', @@ -149,8 +157,7 @@ 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')(({ theme }) => ({ +const TopRow = styled('div')(({ theme }) => ({ display: 'flex', flexFlow: 'row wrap', justifyContent: 'space-between', @@ -178,15 +185,12 @@ const NewNetworkTrafficUsage: FC = () => { const { isOss } = useUiConfig(); const { locationSettings } = useLocationSettings(); - const { - record, - newPeriod, - setNewPeriod, - toTrafficUsageSum, - calculateOverageCost, - calculateEstimatedMonthlyCost, - endpointsInfo, - } = useTrafficDataEstimation(); + + const [chartDataSelection, setChartDataSelection] = + useState({ + grouping: 'daily', + month: selectablePeriods[0].key, + }); const includedTraffic = useTrafficLimit(); @@ -194,8 +198,8 @@ const NewNetworkTrafficUsage: FC = () => { return createBarChartOptions( theme, (tooltipItems: any) => { - if (newPeriod.grouping === 'daily') { - const periodItem = record[newPeriod.month]; + if (chartDataSelection.grouping === 'daily') { + const periodItem = periodsRecord[chartDataSelection.month]; const tooltipDate = new Date( periodItem.year, periodItem.month, @@ -225,65 +229,42 @@ const NewNetworkTrafficUsage: FC = () => { }, includedTraffic, ); - }, [theme, newPeriod]); + }, [theme, chartDataSelection]); - const traffic = useInstanceTrafficMetrics2(newPeriod); + const traffic = useInstanceTrafficMetrics2( + chartDataSelection.grouping, + toDateRange(chartDataSelection), + ); const data = newToChartData(traffic.usage); + const usageTotal = calculateTotalUsage(traffic.usage); + const overageCost = calculateOverageCost( + usageTotal, + includedTraffic, + BILLING_TRAFFIC_BUNDLE_PRICE, + ); - const [usageTotal, setUsageTotal] = useState(0); + const estimatedMonthlyCost = calculateEstimatedMonthlyCost( + chartDataSelection.grouping === 'daily' + ? chartDataSelection.month + : currentMonth, + data.datasets, + includedTraffic, + currentDate, + BILLING_TRAFFIC_BUNDLE_PRICE, + ); - const [overageCost, setOverageCost] = useState(0); + const showOverageCalculations = + chartDataSelection.grouping === 'daily' && + includedTraffic > 0 && + usageTotal - includedTraffic > 0 && + estimateTrafficDataCost; - const [estimatedMonthlyCost, setEstimatedMonthlyCost] = useState(0); - - useEffect(() => { - if (data) { - let usage: number; - if (newPeriod.grouping === 'monthly') { - usage = monthlyTrafficDataToCurrentUsage(traffic.usage); - } else { - usage = toTrafficUsageSum(data.datasets); - } - - setUsageTotal(usage); - if (includedTraffic > 0) { - const calculatedOverageCost = calculateOverageCost( - usage, - includedTraffic, - BILLING_TRAFFIC_BUNDLE_PRICE, - ); - setOverageCost(calculatedOverageCost); - - setEstimatedMonthlyCost( - calculateEstimatedMonthlyCost( - newPeriod.grouping === 'daily' - ? newPeriod.month - : format(new Date(), 'yyyy-MM'), - data.datasets, - includedTraffic, - new Date(), - BILLING_TRAFFIC_BUNDLE_PRICE, - ), - ); - } - } - }, [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.`; + const showConsumptionBillingWarning = + (chartDataSelection.grouping === 'monthly' || + chartDataSelection.month === currentMonth) && + includedTraffic > 0 && + overageCost > 0; return ( { elseShow={ <> 0 && - overageCost > 0 - } + condition={showConsumptionBillingWarning} show={ Heads up! You are currently @@ -318,12 +293,12 @@ const NewNetworkTrafficUsage: FC = () => { } /> - + { } includedTraffic={includedTraffic} /> - {newPeriod.grouping === 'daily' && - includedTraffic > 0 && - usageTotal - includedTraffic > 0 && - estimateTrafficDataCost && ( - - )} + {showOverageCalculations && ( + + )} - + @@ -390,8 +360,6 @@ const OldNetworkTrafficUsage: FC = () => { toChartData, toTrafficUsageSum, endpointsInfo, - calculateOverageCost, - calculateEstimatedMonthlyCost, } = useTrafficDataEstimation(); const includedTraffic = useTrafficLimit(); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx index 3e1c364067..caf2391fb8 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx @@ -1,70 +1,14 @@ import { styled, Button, Popover, Box, type Theme } from '@mui/material'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp'; -import type { ChartDataSelection } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import { useRef, useState, type FC } from 'react'; import { format } from 'date-fns'; +import type { ChartDataSelection } from './chart-data-selection'; +import { selectablePeriods } from './selectable-periods'; const dropdownWidth = '15rem'; const dropdownInlinePadding = (theme: Theme) => theme.spacing(3); -export type Period = { - key: string; - dayCount: number; - label: string; - year: number; - month: number; - selectable: boolean; - shortLabel: string; -}; - -export const toSelectablePeriod = ( - date: Date, - label?: string, - selectable = true, -): Period => { - const year = date.getFullYear(); - const month = date.getMonth(); - const period = `${year}-${(month + 1).toString().padStart(2, '0')}`; - const dayCount = new Date(year, month + 1, 0).getDate(); - return { - key: period, - year, - month, - dayCount, - shortLabel: date.toLocaleString('en-US', { - month: 'short', - }), - label: - label || - date.toLocaleString('en-US', { month: 'long', year: 'numeric' }), - selectable, - }; -}; - -const currentDate = new Date(Date.now()); -const currentPeriod = toSelectablePeriod(currentDate, 'Current month'); - -const getSelectablePeriods = (): Period[] => { - const selectablePeriods = [currentPeriod]; - for ( - let subtractMonthCount = 1; - subtractMonthCount < 12; - subtractMonthCount++ - ) { - // JavaScript wraps around the year, so we don't need to handle that. - const date = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - subtractMonthCount, - 1, - ); - selectablePeriods.push( - toSelectablePeriod(date, undefined, date > new Date('2024-03-31')), - ); - } - return selectablePeriods; -}; - const Wrapper = styled('article')(({ theme }) => ({ width: dropdownWidth, paddingBlock: theme.spacing(2), @@ -167,8 +111,6 @@ const StyledPopover = styled(Popover)(({ theme }) => ({ })); export const PeriodSelector: FC = ({ selectedPeriod, setPeriod }) => { - const selectablePeriods = getSelectablePeriods(); - const rangeOptions = [3, 6, 12].map((monthsBack) => ({ value: monthsBack, label: `Last ${monthsBack} months`, diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx index da6531dea0..33b35c2869 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/RequestSummary.tsx @@ -1,9 +1,9 @@ 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'; +import type { ChartDataSelection } from './chart-data-selection'; type Props = { period: ChartDataSelection; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-data-selection.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-data-selection.ts new file mode 100644 index 0000000000..291109c27f --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-data-selection.ts @@ -0,0 +1,29 @@ +import { endOfMonth, format, startOfMonth, subMonths } from 'date-fns'; + +export type ChartDataSelection = + | { + grouping: 'daily'; + month: string; + } + | { + grouping: 'monthly'; + monthsBack: number; + }; + +// todo: write test +export const toDateRange = ( + selection: ChartDataSelection, +): { from: string; to: string } => { + const fmt = (date: Date) => format(date, 'yyyy-MM-dd'); + if (selection.grouping === 'daily') { + const month = new Date(selection.month); + const from = fmt(month); + const to = fmt(endOfMonth(month)); + return { from, to }; + } else { + const now = new Date(); + const from = fmt(startOfMonth(subMonths(now, selection.monthsBack))); + const to = fmt(endOfMonth(now)); + return { from, to }; + } +}; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts new file mode 100644 index 0000000000..d860d12e64 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts @@ -0,0 +1,136 @@ +import type { ChartDataset } from 'chart.js'; +import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi'; +import { endpointsInfo } from './endpoint-info'; +import { + addDays, + addMonths, + differenceInCalendarDays, + differenceInCalendarMonths, +} from 'date-fns'; +import { formatDay, formatMonth } from './dates'; +import type { ChartDataSelection } from './chart-data-selection'; +export type ChartDatasetType = ChartDataset<'bar'>; + +// todo: test +export const toChartData = ( + traffic?: TrafficUsageDataSegmentedCombinedSchema, +): { datasets: ChartDatasetType[]; labels: (string | number)[] } => { + if (!traffic) { + return { labels: [], datasets: [] }; + } + + if (traffic.grouping === 'monthly') { + return toMonthlyChartData(traffic); + } else { + return toDailyChartData(traffic); + } +}; + +type SegmentedSchemaApiData = + TrafficUsageDataSegmentedCombinedSchema['apiData'][0]; + +// todo: integrate filtering `filterData` frontend/src/component/admin/network/NetworkTrafficUsage/util.ts +const prepareApiData = ( + apiData: TrafficUsageDataSegmentedCombinedSchema['apiData'], +) => + apiData + .filter((item) => item.apiPath in endpointsInfo) + .sort( + (item1: SegmentedSchemaApiData, item2: SegmentedSchemaApiData) => + endpointsInfo[item1.apiPath].order - + endpointsInfo[item2.apiPath].order, + ); + +const toMonthlyChartData = ( + traffic: TrafficUsageDataSegmentedCombinedSchema, +): { datasets: ChartDatasetType[]; labels: string[] } => { + const from = new Date(traffic.dateRange.from); + const to = new Date(traffic.dateRange.to); + const numMonths = Math.abs(differenceInCalendarMonths(to, from)) + 1; + + const datasets = prepareApiData(traffic.apiData).map( + (item: SegmentedSchemaApiData) => { + const monthsRec: { [month: string]: number } = {}; + for (let i = 0; i < numMonths; i++) { + monthsRec[formatMonth(addMonths(from, i))] = 0; + } + + for (const month of Object.values(item.dataPoints)) { + monthsRec[month.period] = month.trafficTypes[0].count; + } + + const epInfo = endpointsInfo[item.apiPath]; + + return { + label: epInfo.label, + data: Object.values(monthsRec), + backgroundColor: epInfo.color, + hoverBackgroundColor: epInfo.color, + }; + }, + ); + + const labels = Array.from({ length: numMonths }).map((_, index) => + index === numMonths - 1 + ? 'Current month' + : formatMonth(addMonths(from, index)), + ); + + return { datasets, labels }; +}; + +const toDailyChartData = ( + traffic: TrafficUsageDataSegmentedCombinedSchema, +): { datasets: ChartDatasetType[]; labels: number[] } => { + const from = new Date(traffic.dateRange.from); + const to = new Date(traffic.dateRange.to); + const numDays = Math.abs(differenceInCalendarDays(to, from)) + 1; + + const daysRec: { [day: string]: number } = {}; + for (let i = 0; i < numDays; i++) { + daysRec[formatDay(addDays(from, i))] = 0; + } + + const getDaysRec = () => ({ + ...daysRec, + }); + + const datasets = prepareApiData(traffic.apiData).map( + (item: SegmentedSchemaApiData) => { + const daysRec = getDaysRec(); + + for (const day of Object.values(item.dataPoints)) { + daysRec[day.period] = day.trafficTypes[0].count; + } + + const epInfo = endpointsInfo[item.apiPath]; + + return { + label: epInfo.label, + data: Object.values(daysRec), + backgroundColor: epInfo.color, + hoverBackgroundColor: epInfo.color, + }; + }, + ); + + // simplification: assuming days run in a single month from the 1st onwards + const labels = Array.from({ length: numDays }).map((_, index) => index + 1); + + return { datasets, labels }; +}; + +const [lastLabel, ...otherLabels] = Object.values(endpointsInfo) + .map((info) => info.label.toLowerCase()) + .toReversed(); +const requestTypes = `${otherLabels.toReversed().join(', ')}, and ${lastLabel}`; + +export const getChartLabel = (selectedPeriod: ChartDataSelection) => + selectedPeriod.grouping === 'daily' + ? `A bar chart showing daily traffic usage for ${new Date( + selectedPeriod.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 ${selectedPeriod.monthsBack} months. Each month shows ${requestTypes} requests.`; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/dates.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/dates.ts new file mode 100644 index 0000000000..0976cd2a12 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/dates.ts @@ -0,0 +1,13 @@ +import { format, getDaysInMonth } from 'date-fns'; + +export const currentDate = new Date(); + +// making this a constant instead of a function. This avoids re-calculating and +// formatting every time it's called, but it does introduce some edge cases +// where it might stay the same across component renders. +export const currentMonth = format(currentDate, 'yyyy-MM'); + +export const daysInCurrentMonth = getDaysInMonth(currentDate); + +export const formatMonth = (date: Date) => format(date, 'yyyy-MM'); +export const formatDay = (date: Date) => format(date, 'yyyy-MM-dd'); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/endpoint-info.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/endpoint-info.ts new file mode 100644 index 0000000000..a4849e6a10 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/endpoint-info.ts @@ -0,0 +1,23 @@ +export type EndpointInfo = { + label: string; + color: string; + order: number; +}; + +export 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, + }, +}; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/selectable-periods.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/selectable-periods.ts new file mode 100644 index 0000000000..936a4cfef2 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/selectable-periods.ts @@ -0,0 +1,56 @@ +import { getDaysInMonth, subMonths } from 'date-fns'; +import { currentDate, formatMonth } from './dates'; + +export type Period = { + key: string; + dayCount: number; + label: string; + year: number; + month: number; + selectable: boolean; + shortLabel: string; +}; + +export const toSelectablePeriod = ( + date: Date, + label?: string, + selectable = true, +): Period => { + const year = date.getFullYear(); + const month = date.getMonth(); + const period = formatMonth(date); + const dayCount = getDaysInMonth(date); + return { + key: period, + year, + month, + dayCount, + shortLabel: date.toLocaleString('en-US', { + month: 'short', + }), + label: + label || + date.toLocaleString('en-US', { month: 'long', year: 'numeric' }), + selectable, + }; +}; + +// todo: test +const generateSelectablePeriodsFromDate = (now: Date) => { + const selectablePeriods = [toSelectablePeriod(now, 'Current month')]; + for ( + let subtractMonthCount = 1; + subtractMonthCount < 12; + subtractMonthCount++ + ) { + const date = subMonths(now, subtractMonthCount); + selectablePeriods.push( + toSelectablePeriod(date, undefined, date >= new Date('2024-05')), + ); + } + return selectablePeriods; +}; +export const selectablePeriods = generateSelectablePeriodsFromDate(currentDate); +export const periodsRecord = Object.fromEntries( + selectablePeriods.map((period) => [period.key, period]), +); diff --git a/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts b/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts index dee5ae96c8..c81da21c9e 100644 --- a/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts +++ b/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts @@ -4,10 +4,8 @@ import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import type { TrafficUsageDataSegmentedCombinedSchema, - TrafficUsageDataSegmentedCombinedSchemaApiDataItem, TrafficUsageDataSegmentedSchema, } from 'openapi'; -import { endOfMonth, format, startOfMonth, subMonths } from 'date-fns'; export interface IInstanceTrafficMetricsResponse { usage: TrafficUsageDataSegmentedSchema; @@ -38,34 +36,6 @@ export const useInstanceTrafficMetrics = ( ); }; -export type ChartDataSelection = - | { - grouping: 'daily'; - month: string; - } - | { - grouping: 'monthly'; - monthsBack: number; - }; - -const fromSelection = (selection: ChartDataSelection) => { - const fmt = (date: Date) => format(date, 'yyyy-MM-dd'); - if (selection.grouping === 'daily') { - const month = new Date(selection.month); - const from = fmt(month); - const to = fmt(endOfMonth(month)); - return { from, to }; - } else { - const now = new Date(); - const from = fmt(startOfMonth(subMonths(now, selection.monthsBack))); - const to = fmt(endOfMonth(now)); - return { from, to }; - } -}; - -export type SegmentedSchemaApiData = - TrafficUsageDataSegmentedCombinedSchemaApiDataItem; - export type InstanceTrafficMetricsResponse2 = { usage: TrafficUsageDataSegmentedCombinedSchema; @@ -77,11 +47,16 @@ export type InstanceTrafficMetricsResponse2 = { }; export const useInstanceTrafficMetrics2 = ( - selection: ChartDataSelection, + grouping: 'monthly' | 'daily', + { + from, + to, + }: { + from: string; + to: string; + }, ): InstanceTrafficMetricsResponse2 => { - const { from, to } = fromSelection(selection); - - const apiPath = `api/admin/metrics/traffic-search?grouping=${selection.grouping}&from=${from}&to=${to}`; + const apiPath = `api/admin/metrics/traffic-search?grouping=${grouping}&from=${from}&to=${to}`; const { data, error, mutate } = useSWR(formatApiPath(apiPath), fetcher); diff --git a/frontend/src/hooks/useTrafficData.ts b/frontend/src/hooks/useTrafficData.ts index a70a86de5e..80bace0719 100644 --- a/frontend/src/hooks/useTrafficData.ts +++ b/frontend/src/hooks/useTrafficData.ts @@ -1,21 +1,6 @@ +import type { ChartDatasetType } from 'component/admin/network/NetworkTrafficUsage/chart-functions'; +import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import { useState } from 'react'; -import type { - ChartDataSelection, - IInstanceTrafficMetricsResponse, - SegmentedSchemaApiData, -} from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; -import type { ChartDataset } from 'chart.js'; -import { - addDays, - addMonths, - differenceInCalendarDays, - differenceInCalendarMonths, - format, -} from 'date-fns'; -import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi'; - -const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5; -const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000; export type SelectablePeriod = { key: string; @@ -31,8 +16,6 @@ export type EndpointInfo = { order: number; }; -export type ChartDatasetType = ChartDataset<'bar'>; - const endpointsInfo: Record = { '/api/admin': { label: 'Admin', @@ -51,15 +34,6 @@ const endpointsInfo: Record = { }, }; -const calculateTrafficDataCost = ( - trafficData: number, - trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, - trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, -) => { - const unitCount = Math.ceil(trafficData / trafficUnitSize); - return unitCount * trafficUnitCost; -}; - const padMonth = (month: number): string => month.toString().padStart(2, '0'); export const toSelectablePeriod = ( @@ -115,113 +89,6 @@ const toPeriodsRecord = ( {} as Record, ); }; - -export const newToChartData = ( - traffic?: TrafficUsageDataSegmentedCombinedSchema, -): { datasets: ChartDatasetType[]; labels: (string | number)[] } => { - if (!traffic) { - return { labels: [], datasets: [] }; - } - - if (traffic.grouping === 'monthly') { - return toMonthlyChartData(traffic); - } else { - return toDailyChartData(traffic); - } -}; - -const prepareApiData = ( - apiData: TrafficUsageDataSegmentedCombinedSchema['apiData'], -) => - apiData - .filter((item) => item.apiPath in endpointsInfo) - .sort( - (item1: SegmentedSchemaApiData, item2: SegmentedSchemaApiData) => - endpointsInfo[item1.apiPath].order - - endpointsInfo[item2.apiPath].order, - ); - -const toMonthlyChartData = ( - traffic: TrafficUsageDataSegmentedCombinedSchema, -): { datasets: ChartDatasetType[]; labels: string[] } => { - const from = new Date(traffic.dateRange.from); - const to = new Date(traffic.dateRange.to); - const numMonths = Math.abs(differenceInCalendarMonths(to, from)) + 1; - const formatMonth = (date: Date) => format(date, 'yyyy-MM'); - - const datasets = prepareApiData(traffic.apiData).map( - (item: SegmentedSchemaApiData) => { - const monthsRec: { [month: string]: number } = {}; - for (let i = 0; i < numMonths; i++) { - monthsRec[formatMonth(addMonths(from, i))] = 0; - } - - for (const month of Object.values(item.dataPoints)) { - monthsRec[month.period] = month.trafficTypes[0].count; - } - - const epInfo = endpointsInfo[item.apiPath]; - - return { - label: epInfo.label, - data: Object.values(monthsRec), - backgroundColor: epInfo.color, - hoverBackgroundColor: epInfo.color, - }; - }, - ); - - const labels = Array.from({ length: numMonths }).map((_, index) => - index === numMonths - 1 - ? 'Current month' - : formatMonth(addMonths(from, index)), - ); - - return { datasets, labels }; -}; - -const toDailyChartData = ( - traffic: TrafficUsageDataSegmentedCombinedSchema, -): { datasets: ChartDatasetType[]; labels: number[] } => { - const from = new Date(traffic.dateRange.from); - const to = new Date(traffic.dateRange.to); - const numDays = Math.abs(differenceInCalendarDays(to, from)) + 1; - const formatDay = (date: Date) => format(date, 'yyyy-MM-dd'); - - const daysRec: { [day: string]: number } = {}; - for (let i = 0; i < numDays; i++) { - daysRec[formatDay(addDays(from, i))] = 0; - } - - const getDaysRec = () => ({ - ...daysRec, - }); - - const datasets = prepareApiData(traffic.apiData).map( - (item: SegmentedSchemaApiData) => { - const daysRec = getDaysRec(); - - for (const day of Object.values(item.dataPoints)) { - daysRec[day.period] = day.trafficTypes[0].count; - } - - const epInfo = endpointsInfo[item.apiPath]; - - return { - label: epInfo.label, - data: Object.values(daysRec), - backgroundColor: epInfo.color, - hoverBackgroundColor: epInfo.color, - }; - }, - ); - - // simplification: assuming days run in a single month from the 1st onwards - const labels = Array.from({ length: numDays }).map((_, index) => index + 1); - - return { datasets, labels }; -}; - const toChartData = ( days: number[], traffic: IInstanceTrafficMetricsResponse, @@ -285,94 +152,20 @@ const getDayLabels = (dayCount: number): number[] => { return [...Array(dayCount).keys()].map((i) => i + 1); }; -export const calculateOverageCost = ( - dataUsage: number, - includedTraffic: number, - trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, - trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, -): number => { - if (dataUsage === 0) { - return 0; - } - - const overage = - Math.floor((dataUsage - includedTraffic) / 1_000_000) * 1_000_000; - return overage > 0 - ? calculateTrafficDataCost(overage, trafficUnitCost, trafficUnitSize) - : 0; -}; - -export const calculateProjectedUsage = ( - today: number, - trafficData: ChartDatasetType[], - daysInPeriod: number, -) => { - if (today < 5) { - return 0; - } - - const spliceToYesterday = today - 1; - const trafficDataUpToYesterday = trafficData.map((item) => { - return { - ...item, - data: item.data.slice(0, spliceToYesterday), - }; - }); - - const dataUsage = toTrafficUsageSum(trafficDataUpToYesterday); - return (dataUsage / spliceToYesterday) * daysInPeriod; -}; - -export const calculateEstimatedMonthlyCost = ( - period: string, - trafficData: ChartDatasetType[], - includedTraffic: number, - currentDate: Date, - trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, - trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, -) => { - if (period !== currentPeriod.key) { - return 0; - } - - const today = currentDate.getDate(); - const projectedUsage = calculateProjectedUsage( - today, - trafficData, - currentPeriod.dayCount, - ); - return calculateOverageCost( - projectedUsage, - includedTraffic, - trafficUnitCost, - trafficUnitSize, - ); -}; - export const useTrafficDataEstimation = () => { const selectablePeriods = getSelectablePeriods(); const record = toPeriodsRecord(selectablePeriods); const [period, setPeriod] = useState(selectablePeriods[0].key); - const [newPeriod, setNewPeriod] = useState({ - grouping: 'daily', - month: selectablePeriods[0].key, - }); - return { - calculateTrafficDataCost, record, period, setPeriod, - newPeriod, - setNewPeriod, selectablePeriods, getDayLabels, currentPeriod, toChartData, toTrafficUsageSum, endpointsInfo, - calculateOverageCost, - calculateEstimatedMonthlyCost, }; }; diff --git a/frontend/src/hooks/useTrafficData.test.ts b/frontend/src/utils/traffic-calculations.test.ts similarity index 97% rename from frontend/src/hooks/useTrafficData.test.ts rename to frontend/src/utils/traffic-calculations.test.ts index 1ea416e39e..5879909086 100644 --- a/frontend/src/hooks/useTrafficData.test.ts +++ b/frontend/src/utils/traffic-calculations.test.ts @@ -1,10 +1,10 @@ import { getDaysInMonth } from 'date-fns'; import { - toSelectablePeriod, - calculateOverageCost, calculateEstimatedMonthlyCost, + calculateOverageCost, calculateProjectedUsage, -} from './useTrafficData'; +} from './traffic-calculations'; +import { toSelectablePeriod } from '../component/admin/network/NetworkTrafficUsage/selectable-periods'; const testData4Days = [ { diff --git a/frontend/src/utils/traffic-calculations.ts b/frontend/src/utils/traffic-calculations.ts new file mode 100644 index 0000000000..3a78c0a6c3 --- /dev/null +++ b/frontend/src/utils/traffic-calculations.ts @@ -0,0 +1,157 @@ +import type { + TrafficUsageDataSegmentedCombinedSchema, + TrafficUsageDataSegmentedCombinedSchemaApiDataItem, +} from 'openapi'; +import { + currentMonth, + daysInCurrentMonth, +} from '../component/admin/network/NetworkTrafficUsage/dates'; +import type { ChartDatasetType } from '../component/admin/network/NetworkTrafficUsage/chart-functions'; + +const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5; +const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000; + +// todo: implement and test +export const filterData = ( + data?: TrafficUsageDataSegmentedCombinedSchema, +): TrafficUsageDataSegmentedCombinedSchema | undefined => { + if (!data) { + return; + } + // filter out endpoints not mentioned in endpointsInfo + // filter out any data from before May 2024 + return data; +}; + +const monthlyTrafficDataToCurrentUsage = ( + apiData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[], +) => { + return apiData.reduce((acc, current) => { + const currentPoint = current.dataPoints.find( + ({ period }) => period === currentMonth, + ); + const pointUsage = + currentPoint?.trafficTypes.reduce( + (acc, next) => acc + next.count, + 0, + ) ?? 0; + return acc + pointUsage; + }, 0); +}; + +const dailyTrafficDataToCurrentUsage = ( + apiData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[], +) => { + return apiData + .flatMap((endpoint) => + endpoint.dataPoints.flatMap((dataPoint) => + dataPoint.trafficTypes.map((trafficType) => trafficType.count), + ), + ) + .reduce((acc, count) => acc + count, 0); +}; + +// todo: test +// Return the total number of requests for the selected month if showing daily +// data, or the current month if showing monthly data +export const calculateTotalUsage = ( + data?: TrafficUsageDataSegmentedCombinedSchema, +): number => { + if (!data) { + return 0; + } + const { grouping, apiData } = data; + return grouping === 'monthly' + ? monthlyTrafficDataToCurrentUsage(apiData) + : dailyTrafficDataToCurrentUsage(apiData); +}; + +const calculateTrafficDataCost = ( + trafficData: number, + trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, + trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, +) => { + const unitCount = Math.ceil(trafficData / trafficUnitSize); + return unitCount * trafficUnitCost; +}; + +export const calculateOverageCost = ( + dataUsage: number, + includedTraffic: number, + trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, + trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, +): number => { + if (dataUsage === 0) { + return 0; + } + + const overage = + Math.floor((dataUsage - includedTraffic) / 1_000_000) * 1_000_000; + return overage > 0 + ? calculateTrafficDataCost(overage, trafficUnitCost, trafficUnitSize) + : 0; +}; + +export const calculateProjectedUsage = ( + today: number, + trafficData: ChartDatasetType[], + daysInPeriod: number, +) => { + if (today < 5) { + return 0; + } + + const spliceToYesterday = today - 1; + const trafficDataUpToYesterday = trafficData.map((item) => { + return { + ...item, + data: item.data.slice(0, spliceToYesterday), + }; + }); + + const toTrafficUsageSum = (trafficData: ChartDatasetType[]): number => { + const data = trafficData.reduce( + (acc: number, current: ChartDatasetType) => { + return ( + acc + + current.data.reduce( + (acc_inner, current_inner) => acc_inner + current_inner, + 0, + ) + ); + }, + 0, + ); + return data; + }; + + const dataUsage = toTrafficUsageSum(trafficDataUpToYesterday); + return (dataUsage / spliceToYesterday) * daysInPeriod; +}; + +export const calculateEstimatedMonthlyCost = ( + period: string, + trafficData: ChartDatasetType[], + includedTraffic: number, + currentDate: Date, + trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, + trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, +) => { + if (period !== currentMonth) { + return 0; + } + + const today = currentDate.getDate(); + const projectedUsage = calculateProjectedUsage( + today, + trafficData, + daysInCurrentMonth, + ); + + return calculateOverageCost( + projectedUsage, + includedTraffic, + trafficUnitCost, + trafficUnitSize, + ); +};