1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

refactor(1-3336): useTrafficData / NetworkTrafficUsage.tsx cleanup (#9191)

This PR refactors the `NetworkTrafficUsage.tsx` and `useTrafficData`
files a bit.

The primary objective was to make the network traffic usage component
easier to work with, so I suggest to the reviewer that they start there.

Part of that refactoring, was taking things out of the useTraffic hook
that didn't need to be there. In the end, I'd removed so much that I
didn't even need the hook itself in the new component, so I switched
that to a regular useState.

It made more sense to me to put some of the functions inside the hook
into a separate file and import them directly (because they don't rely
on any hook state), so I have done that and removed those functions from
the trafficData hook. In this case, I also moved the tests.

I have not added any new tests in this PR, but will do so in a
follow-up. The functions I intend to test have been marked as such.
This commit is contained in:
Thomas Heartman 2025-02-04 10:32:59 +01:00 committed by GitHub
parent c1e41b2b05
commit 2980c0de4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 504 additions and 412 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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<ChartDataSelection>({
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<number>(0);
const estimatedMonthlyCost = calculateEstimatedMonthlyCost(
chartDataSelection.grouping === 'daily'
? chartDataSelection.month
: currentMonth,
data.datasets,
includedTraffic,
currentDate,
BILLING_TRAFFIC_BUNDLE_PRICE,
);
const [overageCost, setOverageCost] = useState<number>(0);
const showOverageCalculations =
chartDataSelection.grouping === 'daily' &&
includedTraffic > 0 &&
usageTotal - includedTraffic > 0 &&
estimateTrafficDataCost;
const [estimatedMonthlyCost, setEstimatedMonthlyCost] = useState<number>(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 (
<ConditionallyRender
@ -292,13 +273,7 @@ const NewNetworkTrafficUsage: FC = () => {
elseShow={
<>
<ConditionallyRender
condition={
(newPeriod.grouping === 'monthly' ||
newPeriod.month ===
format(new Date(), 'yyyy-MM')) &&
includedTraffic > 0 &&
overageCost > 0
}
condition={showConsumptionBillingWarning}
show={
<Alert severity='warning' sx={{ mb: 4 }}>
<BoldText>Heads up!</BoldText> You are currently
@ -318,12 +293,12 @@ const NewNetworkTrafficUsage: FC = () => {
}
/>
<StyledBox>
<NewHeader>
<TopRow>
<TrafficInfoBoxes>
<RequestSummary
period={newPeriod}
period={chartDataSelection}
usageTotal={
newPeriod.grouping === 'daily'
chartDataSelection.grouping === 'daily'
? usageTotal
: averageTrafficPreviousMonths(
Object.keys(endpointsInfo),
@ -332,31 +307,26 @@ const NewNetworkTrafficUsage: FC = () => {
}
includedTraffic={includedTraffic}
/>
{newPeriod.grouping === 'daily' &&
includedTraffic > 0 &&
usageTotal - includedTraffic > 0 &&
estimateTrafficDataCost && (
<OverageInfo
overageCost={overageCost}
overages={
usageTotal - includedTraffic
}
estimatedMonthlyCost={
estimatedMonthlyCost
}
/>
)}
{showOverageCalculations && (
<OverageInfo
overageCost={overageCost}
overages={usageTotal - includedTraffic}
estimatedMonthlyCost={
estimatedMonthlyCost
}
/>
)}
</TrafficInfoBoxes>
<PeriodSelector
selectedPeriod={newPeriod}
setPeriod={setNewPeriod}
selectedPeriod={chartDataSelection}
setPeriod={setChartDataSelection}
/>
</NewHeader>
</TopRow>
<Bar
data={data}
plugins={[customHighlightPlugin()]}
options={options}
aria-label={chartLabel}
aria-label={getChartLabel(chartDataSelection)}
/>
</StyledBox>
</>
@ -390,8 +360,6 @@ const OldNetworkTrafficUsage: FC = () => {
toChartData,
toTrafficUsageSum,
endpointsInfo,
calculateOverageCost,
calculateEstimatedMonthlyCost,
} = useTrafficDataEstimation();
const includedTraffic = useTrafficLimit();

View File

@ -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<Props> = ({ selectedPeriod, setPeriod }) => {
const selectablePeriods = getSelectablePeriods();
const rangeOptions = [3, 6, 12].map((monthsBack) => ({
value: monthsBack,
label: `Last ${monthsBack} months`,

View File

@ -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;

View File

@ -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 };
}
};

View File

@ -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.`;

View File

@ -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');

View File

@ -0,0 +1,23 @@
export type EndpointInfo = {
label: string;
color: string;
order: number;
};
export const endpointsInfo: Record<string, EndpointInfo> = {
'/api/admin': {
label: 'Admin',
color: '#6D66D9',
order: 1,
},
'/api/frontend': {
label: 'Frontend',
color: '#A39EFF',
order: 2,
},
'/api/client': {
label: 'Server',
color: '#D8D6FF',
order: 3,
},
};

View File

@ -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]),
);

View File

@ -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);

View File

@ -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<string, EndpointInfo> = {
'/api/admin': {
label: 'Admin',
@ -51,15 +34,6 @@ const endpointsInfo: Record<string, EndpointInfo> = {
},
};
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<string, SelectablePeriod>,
);
};
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<string>(selectablePeriods[0].key);
const [newPeriod, setNewPeriod] = useState<ChartDataSelection>({
grouping: 'daily',
month: selectablePeriods[0].key,
});
return {
calculateTrafficDataCost,
record,
period,
setPeriod,
newPeriod,
setNewPeriod,
selectablePeriods,
getDayLabels,
currentPeriod,
toChartData,
toTrafficUsageSum,
endpointsInfo,
calculateOverageCost,
calculateEstimatedMonthlyCost,
};
};

View File

@ -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 = [
{

View File

@ -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,
);
};