mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-28 00:06:53 +01:00
feat: refactor data usage into hooks, estimate monthly added fees (#7048)
- Refactors data processing and overage calculations to separate hooks - Adds support for estimating traffic costs based on monthly usage up to current point - Adds accrued traffic charges to the billing page ![image](https://github.com/Unleash/unleash/assets/707867/39a837c2-5092-49b8-8bbf-46d8757635c0) ![image](https://github.com/Unleash/unleash/assets/707867/55ecfa5e-afe1-4cb6-9aa4-7dd67db4248c)
This commit is contained in:
parent
5c4b835cb5
commit
dfc0c3c63f
@ -1,4 +1,5 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { Alert, Divider, Grid, styled, Typography } from '@mui/material';
|
import { Alert, Divider, Grid, styled, Typography } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
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 { GridCol } from 'component/common/GridCol/GridCol';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { GridColLink } from './GridColLink/GridColLink';
|
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 }) => ({
|
const StyledPlanBox = styled('aside')(({ theme }) => ({
|
||||||
padding: theme.spacing(2.5),
|
padding: theme.spacing(2.5),
|
||||||
@ -71,10 +75,21 @@ interface IBillingPlanProps {
|
|||||||
instanceStatus: IInstanceStatus;
|
instanceStatus: IInstanceStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proPlanIncludedRequests = 53_000_000;
|
||||||
|
|
||||||
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const expired = trialHasExpired(instanceStatus);
|
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);
|
const eligibleUsers = users.filter((user: any) => user.email);
|
||||||
|
|
||||||
@ -94,6 +109,32 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
const paidAssignedPrice = price.user * paidAssigned;
|
const paidAssignedPrice = price.user * paidAssigned;
|
||||||
const finalPrice = planPrice + paidAssignedPrice;
|
const finalPrice = planPrice + paidAssignedPrice;
|
||||||
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
|
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 (
|
return (
|
||||||
<Grid item xs={12} md={7}>
|
<Grid item xs={12} md={7}>
|
||||||
@ -185,7 +226,11 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
<GridRow>
|
<GridRow
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginBottom: theme.spacing(1.5),
|
||||||
|
})}
|
||||||
|
>
|
||||||
<GridCol vertical>
|
<GridCol vertical>
|
||||||
<Typography>
|
<Typography>
|
||||||
<strong>Paid members</strong>
|
<strong>Paid members</strong>
|
||||||
@ -210,6 +255,40 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={flagEnabled && overageCost > 0}
|
||||||
|
show={
|
||||||
|
<GridRow>
|
||||||
|
<GridCol vertical>
|
||||||
|
<Typography>
|
||||||
|
<strong>
|
||||||
|
Accrued traffic charges
|
||||||
|
</strong>
|
||||||
|
<GridColLink>
|
||||||
|
<Link to='/admin/network/data-usage'>
|
||||||
|
view details
|
||||||
|
</Link>
|
||||||
|
</GridColLink>
|
||||||
|
</Typography>
|
||||||
|
<StyledInfoLabel>
|
||||||
|
$5 dollar per 1 million
|
||||||
|
started above included data
|
||||||
|
</StyledInfoLabel>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontSize:
|
||||||
|
theme.fontSizes
|
||||||
|
.mainHeader,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${overageCost.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
</GridRow>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
<Grid container>
|
<Grid container>
|
||||||
@ -223,7 +302,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
theme.fontSizes.mainHeader,
|
theme.fontSizes.mainHeader,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Total per month
|
Total
|
||||||
</Typography>
|
</Typography>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol>
|
<GridCol>
|
||||||
@ -234,7 +313,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
fontSize: '2rem',
|
fontSize: '2rem',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${finalPrice.toFixed(2)}
|
${totalCost.toFixed(2)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
|
@ -4,7 +4,8 @@ import styled from '@mui/material/styles/styled';
|
|||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import Select from 'component/common/select';
|
import Select from 'component/common/select';
|
||||||
import Box from '@mui/system/Box';
|
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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import {
|
import {
|
||||||
@ -13,7 +14,6 @@ import {
|
|||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
BarElement,
|
BarElement,
|
||||||
type ChartDataset,
|
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
@ -22,136 +22,22 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
|
|
||||||
import { Bar } from 'react-chartjs-2';
|
import { Bar } from 'react-chartjs-2';
|
||||||
import {
|
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||||
type IInstanceTrafficMetricsResponse,
|
|
||||||
useInstanceTrafficMetrics,
|
|
||||||
} from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
|
||||||
import type { Theme } from '@mui/material/styles/createTheme';
|
import type { Theme } from '@mui/material/styles/createTheme';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary';
|
import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
|
import {
|
||||||
type ChartDatasetType = ChartDataset<'bar'>;
|
type ChartDatasetType,
|
||||||
|
useTrafficDataEstimation,
|
||||||
type SelectablePeriod = {
|
} from 'hooks/useTrafficData';
|
||||||
key: string;
|
|
||||||
dayCount: number;
|
|
||||||
label: string;
|
|
||||||
year: number;
|
|
||||||
month: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EndpointInfo = {
|
|
||||||
label: string;
|
|
||||||
color: string;
|
|
||||||
order: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: theme.spacing(5),
|
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<string, SelectablePeriod> => {
|
|
||||||
return periods.reduce(
|
|
||||||
(acc, period) => {
|
|
||||||
acc[period.key] = period;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, SelectablePeriod>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDayLabels = (dayCount: number): number[] => {
|
|
||||||
return [...Array(dayCount).keys()].map((i) => i + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toChartData = (
|
|
||||||
days: number[],
|
|
||||||
traffic: IInstanceTrafficMetricsResponse,
|
|
||||||
endpointsInfo: Record<string, EndpointInfo>,
|
|
||||||
): 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<string, number>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 = {
|
const customHighlightPlugin = {
|
||||||
id: 'customLine',
|
id: 'customLine',
|
||||||
beforeDraw: (chart: Chart) => {
|
beforeDraw: (chart: Chart) => {
|
||||||
@ -295,36 +181,27 @@ const createBarChartOptions = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const proPlanIncludedRequests = 53_000_000;
|
const proPlanIncludedRequests = 53_000_000;
|
||||||
|
|
||||||
export const NetworkTrafficUsage: VFC = () => {
|
export const NetworkTrafficUsage: VFC = () => {
|
||||||
usePageTitle('Network - Data Usage');
|
usePageTitle('Network - Data Usage');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const selectablePeriods = getSelectablePeriods();
|
|
||||||
const record = toPeriodsRecord(selectablePeriods);
|
|
||||||
const [period, setPeriod] = useState<string>(selectablePeriods[0].key);
|
|
||||||
|
|
||||||
const { isOss, isPro } = useUiConfig();
|
const { isOss, isPro } = useUiConfig();
|
||||||
|
|
||||||
|
const {
|
||||||
|
record,
|
||||||
|
period,
|
||||||
|
setPeriod,
|
||||||
|
selectablePeriods,
|
||||||
|
getDayLabels,
|
||||||
|
toChartData,
|
||||||
|
toTrafficUsageSum,
|
||||||
|
endpointsInfo,
|
||||||
|
calculateOverageCost,
|
||||||
|
calculateEstimatedMonthlyCost,
|
||||||
|
} = useTrafficDataEstimation();
|
||||||
|
|
||||||
const includedTraffic = isPro() ? proPlanIncludedRequests : 0;
|
const includedTraffic = isPro() ? proPlanIncludedRequests : 0;
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
@ -355,6 +232,10 @@ export const NetworkTrafficUsage: VFC = () => {
|
|||||||
|
|
||||||
const [usageTotal, setUsageTotal] = useState<number>(0);
|
const [usageTotal, setUsageTotal] = useState<number>(0);
|
||||||
|
|
||||||
|
const [overageCost, setOverageCost] = useState<number>(0);
|
||||||
|
|
||||||
|
const [estimatedMonthlyCost, setEstimatedMonthlyCost] = useState<number>(0);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels,
|
labels,
|
||||||
datasets,
|
datasets,
|
||||||
@ -375,20 +256,21 @@ export const NetworkTrafficUsage: VFC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const usage = data.datasets.reduce(
|
const usage = toTrafficUsageSum(data.datasets);
|
||||||
(acc: number, current: ChartDatasetType) => {
|
const calculatedOverageCost = calculateOverageCost(
|
||||||
return (
|
usage,
|
||||||
acc +
|
includedTraffic,
|
||||||
current.data.reduce(
|
|
||||||
(acc_inner, current_inner) =>
|
|
||||||
acc_inner + current_inner,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
setUsageTotal(usage);
|
setUsageTotal(usage);
|
||||||
|
setOverageCost(calculatedOverageCost);
|
||||||
|
setEstimatedMonthlyCost(
|
||||||
|
calculateEstimatedMonthlyCost(
|
||||||
|
period,
|
||||||
|
data.datasets,
|
||||||
|
includedTraffic,
|
||||||
|
new Date(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@ -398,15 +280,40 @@ export const NetworkTrafficUsage: VFC = () => {
|
|||||||
show={<Alert severity='warning'>Not enabled.</Alert>}
|
show={<Alert severity='warning'>Not enabled.</Alert>}
|
||||||
elseShow={
|
elseShow={
|
||||||
<>
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={overageCost > 0}
|
||||||
|
show={
|
||||||
|
<Alert severity='warning' sx={{ mb: 4 }}>
|
||||||
|
<b>Heads up!</b> You are currently consuming
|
||||||
|
more requests than your plan includes and will
|
||||||
|
be billed according to our terms. Please see{' '}
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to='https://www.getunleash.io/pricing'
|
||||||
|
>
|
||||||
|
this page
|
||||||
|
</Link>{' '}
|
||||||
|
for more information. In order to reduce your
|
||||||
|
traffic consumption, you may configure an{' '}
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to='https://docs.getunleash.io/reference/unleash-edge'
|
||||||
|
>
|
||||||
|
Unleash Edge instance
|
||||||
|
</Link>{' '}
|
||||||
|
in your own datacenter.
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<Grid container component='header' spacing={2}>
|
<Grid container component='header' spacing={2}>
|
||||||
<Grid item xs={12} md={10}>
|
<Grid item xs={12} md={10}>
|
||||||
<Grid item xs={7} md={5.5}>
|
<NetworkTrafficUsagePlanSummary
|
||||||
<NetworkTrafficUsagePlanSummary
|
usageTotal={usageTotal}
|
||||||
usageTotal={usageTotal}
|
includedTraffic={includedTraffic}
|
||||||
includedTraffic={includedTraffic}
|
overageCost={overageCost}
|
||||||
/>
|
estimatedMonthlyCost={estimatedMonthlyCost}
|
||||||
</Grid>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={2}>
|
<Grid item xs={12} md={2}>
|
||||||
<Select
|
<Select
|
||||||
|
@ -1,27 +1,38 @@
|
|||||||
import styled from '@mui/material/styles/styled';
|
import styled from '@mui/material/styles/styled';
|
||||||
import Box from '@mui/system/Box';
|
import Box from '@mui/system/Box';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import { flexRow } from 'themes/themeStyles';
|
import Link from '@mui/material/Link';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
|
const StyledContainerGrid = styled(Grid)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledGrid = styled(Grid)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: '1 1',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledColumnGrid = styled(Grid)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: '1 1',
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'row',
|
||||||
|
flex: '1 1',
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
border: `2px solid ${theme.palette.divider}`,
|
border: `2px solid ${theme.palette.divider}`,
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledCardTitleRow = styled(Box)(() => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledCardDescription = styled(Box)(({ theme }) => ({
|
const StyledCardDescription = styled(Box)(({ theme }) => ({
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: theme.spacing(2.5),
|
gap: theme.spacing(2.5),
|
||||||
@ -31,13 +42,13 @@ const StyledCardDescription = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const RowContainer = styled(Box)(({ theme }) => ({
|
const RowContainer = styled(Box)(({ theme }) => ({
|
||||||
...flexRow,
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledNumbersDiv = styled('div')(({ theme }) => ({
|
const StyledNumbersDiv = styled('div')(({ theme }) => ({
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}));
|
}));
|
||||||
@ -45,80 +56,112 @@ const StyledNumbersDiv = styled('div')(({ theme }) => ({
|
|||||||
interface INetworkTrafficUsagePlanSummary {
|
interface INetworkTrafficUsagePlanSummary {
|
||||||
usageTotal: number;
|
usageTotal: number;
|
||||||
includedTraffic: number;
|
includedTraffic: number;
|
||||||
|
overageCost: number;
|
||||||
|
estimatedMonthlyCost: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NetworkTrafficUsagePlanSummary = ({
|
export const NetworkTrafficUsagePlanSummary = ({
|
||||||
usageTotal,
|
usageTotal,
|
||||||
includedTraffic,
|
includedTraffic,
|
||||||
|
overageCost,
|
||||||
|
estimatedMonthlyCost,
|
||||||
}: INetworkTrafficUsagePlanSummary) => {
|
}: INetworkTrafficUsagePlanSummary) => {
|
||||||
const overages = usageTotal - includedTraffic;
|
const overages = usageTotal - includedTraffic;
|
||||||
|
const estimateFlagEnabled = useUiFlag('estimateTrafficDataCost');
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainerGrid container spacing={4}>
|
||||||
<Grid item>
|
<StyledGrid item xs={5.5} md={5.5}>
|
||||||
<StyledCardTitleRow>
|
<StyledContainer>
|
||||||
<b>Number of requests to Unleash</b>
|
<StyledColumnGrid item>
|
||||||
</StyledCardTitleRow>
|
<Box>
|
||||||
<StyledCardDescription>
|
<b>Number of requests to Unleash</b>
|
||||||
<RowContainer>
|
</Box>
|
||||||
Incoming requests selected month{' '}
|
<StyledCardDescription>
|
||||||
<StyledNumbersDiv>
|
<RowContainer>
|
||||||
<ConditionallyRender
|
Incoming requests selected month{' '}
|
||||||
condition={includedTraffic > 0}
|
<StyledNumbersDiv>
|
||||||
show={
|
<Badge
|
||||||
<ConditionallyRender
|
color={
|
||||||
condition={
|
includedTraffic > 0
|
||||||
usageTotal <= includedTraffic
|
? usageTotal <= includedTraffic
|
||||||
|
? 'success'
|
||||||
|
: 'error'
|
||||||
|
: 'neutral'
|
||||||
}
|
}
|
||||||
show={
|
>
|
||||||
<Badge color='success'>
|
|
||||||
{usageTotal.toLocaleString()}{' '}
|
|
||||||
requests
|
|
||||||
</Badge>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<Badge color='error'>
|
|
||||||
{usageTotal.toLocaleString()}{' '}
|
|
||||||
requests
|
|
||||||
</Badge>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<Badge color='neutral'>
|
|
||||||
{usageTotal.toLocaleString()} requests
|
{usageTotal.toLocaleString()} requests
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledNumbersDiv>
|
|
||||||
</RowContainer>
|
|
||||||
</StyledCardDescription>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={includedTraffic > 0}
|
|
||||||
show={
|
|
||||||
<StyledCardDescription>
|
|
||||||
<RowContainer>
|
|
||||||
Included in your plan monthly
|
|
||||||
<StyledNumbersDiv>
|
|
||||||
{includedTraffic.toLocaleString()} requests
|
|
||||||
</StyledNumbersDiv>
|
</StyledNumbersDiv>
|
||||||
</RowContainer>
|
</RowContainer>
|
||||||
</StyledCardDescription>
|
</StyledCardDescription>
|
||||||
}
|
<ConditionallyRender
|
||||||
/>
|
condition={includedTraffic > 0}
|
||||||
<ConditionallyRender
|
show={
|
||||||
condition={includedTraffic > 0 && overages > 0}
|
<StyledCardDescription>
|
||||||
show={
|
<RowContainer>
|
||||||
<StyledCardDescription>
|
Included in your plan monthly
|
||||||
<RowContainer>
|
<StyledNumbersDiv>
|
||||||
Requests overages this month
|
{includedTraffic.toLocaleString()}{' '}
|
||||||
<StyledNumbersDiv>
|
requests
|
||||||
{overages.toLocaleString()} requests
|
</StyledNumbersDiv>
|
||||||
</StyledNumbersDiv>
|
</RowContainer>
|
||||||
</RowContainer>
|
</StyledCardDescription>
|
||||||
</StyledCardDescription>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</StyledColumnGrid>
|
||||||
</Grid>
|
</StyledContainer>
|
||||||
</StyledContainer>
|
</StyledGrid>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
estimateFlagEnabled && includedTraffic > 0 && overages > 0
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<StyledGrid item xs={5.5} md={5.5}>
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledColumnGrid item>
|
||||||
|
<Box>
|
||||||
|
<b>Accrued traffic charges</b>
|
||||||
|
</Box>
|
||||||
|
<StyledCardDescription>
|
||||||
|
<RowContainer>
|
||||||
|
Requests overages this month (
|
||||||
|
<Link href='https://www.getunleash.io/pricing'>
|
||||||
|
pricing
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
<StyledNumbersDiv>
|
||||||
|
{overages.toLocaleString()} requests
|
||||||
|
</StyledNumbersDiv>
|
||||||
|
</RowContainer>
|
||||||
|
<RowContainer>
|
||||||
|
Accrued traffic charges
|
||||||
|
<StyledNumbersDiv>
|
||||||
|
<Badge color='secondary'>
|
||||||
|
{overageCost} USD
|
||||||
|
</Badge>
|
||||||
|
</StyledNumbersDiv>
|
||||||
|
</RowContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={estimatedMonthlyCost > 0}
|
||||||
|
show={
|
||||||
|
<RowContainer>
|
||||||
|
Estimated traffic charges based
|
||||||
|
on current usage
|
||||||
|
<StyledNumbersDiv>
|
||||||
|
<Badge color='secondary'>
|
||||||
|
{estimatedMonthlyCost}{' '}
|
||||||
|
USD
|
||||||
|
</Badge>
|
||||||
|
</StyledNumbersDiv>
|
||||||
|
</RowContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledCardDescription>
|
||||||
|
</StyledColumnGrid>
|
||||||
|
</StyledContainer>
|
||||||
|
</StyledGrid>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledContainerGrid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
90
frontend/src/hooks/useTrafficData.test.ts
Normal file
90
frontend/src/hooks/useTrafficData.test.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
toSelectablePeriod,
|
||||||
|
calculateOverageCost,
|
||||||
|
calculateEstimatedMonthlyCost,
|
||||||
|
calculateProjectedUsage,
|
||||||
|
} from './useTrafficData';
|
||||||
|
|
||||||
|
const testData4Days = [
|
||||||
|
{
|
||||||
|
label: 'Frontend',
|
||||||
|
data: [23_000_000, 22_000_000, 24_000_000, 21_000_000],
|
||||||
|
backgroundColor: 'red',
|
||||||
|
hoverBackgroundColor: 'red',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Admin',
|
||||||
|
data: [23_000_000, 22_000_000, 24_000_000, 21_000_000],
|
||||||
|
backgroundColor: 'red',
|
||||||
|
hoverBackgroundColor: 'red',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SDK',
|
||||||
|
data: [23_000_000, 22_000_000, 24_000_000, 21_000_000],
|
||||||
|
backgroundColor: 'red',
|
||||||
|
hoverBackgroundColor: 'red',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('traffic overage calculation', () => {
|
||||||
|
it('should return 0 if there is no overage this month', () => {
|
||||||
|
const dataUsage = 52_900_000;
|
||||||
|
const includedTraffic = 53_000_000;
|
||||||
|
const result = calculateOverageCost(dataUsage, includedTraffic);
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 5 if overage this month is atleast 1 request above included', () => {
|
||||||
|
const dataUsage = 53_000_001;
|
||||||
|
const includedTraffic = 53_000_000;
|
||||||
|
const result = calculateOverageCost(dataUsage, includedTraffic);
|
||||||
|
expect(result).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesnt estimate when having less than 5 days worth of data', () => {
|
||||||
|
const now = new Date();
|
||||||
|
const period = toSelectablePeriod(now);
|
||||||
|
const testNow = new Date(now.getFullYear(), now.getMonth(), 4);
|
||||||
|
const result = calculateEstimatedMonthlyCost(
|
||||||
|
period.key,
|
||||||
|
testData4Days,
|
||||||
|
53_000_000,
|
||||||
|
testNow,
|
||||||
|
);
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('needs 5 days or more to estimate for the month', () => {
|
||||||
|
const testData = testData4Days;
|
||||||
|
testData[0].data.push(23_000_000);
|
||||||
|
testData[1].data.push(23_000_000);
|
||||||
|
testData[2].data.push(23_000_000);
|
||||||
|
const now = new Date();
|
||||||
|
const period = toSelectablePeriod(now);
|
||||||
|
const testNow = new Date(now.getFullYear(), now.getMonth(), 5);
|
||||||
|
const result = calculateEstimatedMonthlyCost(
|
||||||
|
period.key,
|
||||||
|
testData,
|
||||||
|
53_000_000,
|
||||||
|
testNow,
|
||||||
|
);
|
||||||
|
expect(result).toBeGreaterThan(1430);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('estimates projected data usage', () => {
|
||||||
|
const testData = testData4Days;
|
||||||
|
testData[0].data.push(22_500_000);
|
||||||
|
testData[1].data.push(22_500_000);
|
||||||
|
testData[2].data.push(22_500_000);
|
||||||
|
// Testing April 5th of 2024 (30 days)
|
||||||
|
const now = new Date(2024, 3, 5);
|
||||||
|
const period = toSelectablePeriod(now);
|
||||||
|
const result = calculateProjectedUsage(
|
||||||
|
now.getDate(),
|
||||||
|
testData,
|
||||||
|
period.dayCount,
|
||||||
|
);
|
||||||
|
// 22_500_000 * 3 * 30 = 2_025_000_000
|
||||||
|
expect(result).toBe(2_025_000_000);
|
||||||
|
});
|
||||||
|
});
|
238
frontend/src/hooks/useTrafficData.ts
Normal file
238
frontend/src/hooks/useTrafficData.ts
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||||
|
import type { ChartDataset } from 'chart.js';
|
||||||
|
|
||||||
|
const TRAFFIC_DATA_UNIT_COST = 5;
|
||||||
|
const TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
|
||||||
|
|
||||||
|
export type SelectablePeriod = {
|
||||||
|
key: string;
|
||||||
|
dayCount: number;
|
||||||
|
label: string;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EndpointInfo = {
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChartDatasetType = ChartDataset<'bar'>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTrafficDataCost = (trafficData: number) => {
|
||||||
|
const unitCount = Math.ceil(trafficData / TRAFFIC_DATA_UNIT_SIZE);
|
||||||
|
return unitCount * TRAFFIC_DATA_UNIT_COST;
|
||||||
|
};
|
||||||
|
|
||||||
|
const padMonth = (month: number): string =>
|
||||||
|
month < 10 ? `0${month}` : `${month}`;
|
||||||
|
|
||||||
|
export 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 currentDate = new Date(Date.now());
|
||||||
|
const currentPeriod = toSelectablePeriod(currentDate, 'Current month');
|
||||||
|
|
||||||
|
const getSelectablePeriods = (): SelectablePeriod[] => {
|
||||||
|
const selectablePeriods = [currentPeriod];
|
||||||
|
for (
|
||||||
|
let subtractMonthCount = 1;
|
||||||
|
subtractMonthCount < 13;
|
||||||
|
subtractMonthCount++
|
||||||
|
) {
|
||||||
|
// JavaScript wraps around the year, so we don't need to handle that.
|
||||||
|
const date = new Date(
|
||||||
|
currentDate.getFullYear(),
|
||||||
|
currentDate.getMonth() - subtractMonthCount,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
if (date > new Date('2024-03-31')) {
|
||||||
|
selectablePeriods.push(toSelectablePeriod(date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectablePeriods;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPeriodsRecord = (
|
||||||
|
periods: SelectablePeriod[],
|
||||||
|
): Record<string, SelectablePeriod> => {
|
||||||
|
return periods.reduce(
|
||||||
|
(acc, period) => {
|
||||||
|
acc[period.key] = period;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, SelectablePeriod>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toChartData = (
|
||||||
|
days: number[],
|
||||||
|
traffic: IInstanceTrafficMetricsResponse,
|
||||||
|
endpointsInfo: Record<string, EndpointInfo>,
|
||||||
|
): 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<string, number>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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 getDayLabels = (dayCount: number): number[] => {
|
||||||
|
return [...Array(dayCount).keys()].map((i) => i + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateOverageCost = (
|
||||||
|
dataUsage: number,
|
||||||
|
includedTraffic: number,
|
||||||
|
): number => {
|
||||||
|
if (dataUsage === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overage = dataUsage - includedTraffic;
|
||||||
|
return overage > 0 ? calculateTrafficDataCost(overage) : 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,
|
||||||
|
) => {
|
||||||
|
if (period !== currentPeriod.key) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = currentDate.getDate();
|
||||||
|
const projectedUsage = calculateProjectedUsage(
|
||||||
|
today,
|
||||||
|
trafficData,
|
||||||
|
currentPeriod.dayCount,
|
||||||
|
);
|
||||||
|
return calculateOverageCost(projectedUsage, includedTraffic);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrafficDataEstimation = () => {
|
||||||
|
const selectablePeriods = getSelectablePeriods();
|
||||||
|
const record = toPeriodsRecord(selectablePeriods);
|
||||||
|
const [period, setPeriod] = useState<string>(selectablePeriods[0].key);
|
||||||
|
|
||||||
|
return {
|
||||||
|
calculateTrafficDataCost,
|
||||||
|
record,
|
||||||
|
period,
|
||||||
|
setPeriod,
|
||||||
|
selectablePeriods,
|
||||||
|
getDayLabels,
|
||||||
|
currentPeriod,
|
||||||
|
toChartData,
|
||||||
|
toTrafficUsageSum,
|
||||||
|
endpointsInfo,
|
||||||
|
calculateOverageCost,
|
||||||
|
calculateEstimatedMonthlyCost,
|
||||||
|
};
|
||||||
|
};
|
@ -76,6 +76,7 @@ export type UiFlags = {
|
|||||||
userAccessUIEnabled?: boolean;
|
userAccessUIEnabled?: boolean;
|
||||||
outdatedSdksBanner?: boolean;
|
outdatedSdksBanner?: boolean;
|
||||||
displayTrafficDataUsage?: boolean;
|
displayTrafficDataUsage?: boolean;
|
||||||
|
estimateTrafficDataCost?: boolean;
|
||||||
disableShowContextFieldSelectionValues?: boolean;
|
disableShowContextFieldSelectionValues?: boolean;
|
||||||
projectOverviewRefactorFeedback?: boolean;
|
projectOverviewRefactorFeedback?: boolean;
|
||||||
featureLifecycle?: boolean;
|
featureLifecycle?: boolean;
|
||||||
|
@ -97,6 +97,7 @@ exports[`should create default config 1`] = `
|
|||||||
"enableLicense": false,
|
"enableLicense": false,
|
||||||
"enableLicenseChecker": false,
|
"enableLicenseChecker": false,
|
||||||
"encryptEmails": false,
|
"encryptEmails": false,
|
||||||
|
"estimateTrafficDataCost": false,
|
||||||
"executiveDashboard": false,
|
"executiveDashboard": false,
|
||||||
"executiveDashboardUI": false,
|
"executiveDashboardUI": false,
|
||||||
"extendedUsageMetrics": false,
|
"extendedUsageMetrics": false,
|
||||||
|
@ -41,6 +41,7 @@ export type IFlagKey =
|
|||||||
| 'killScheduledChangeRequestCache'
|
| 'killScheduledChangeRequestCache'
|
||||||
| 'collectTrafficDataUsage'
|
| 'collectTrafficDataUsage'
|
||||||
| 'displayTrafficDataUsage'
|
| 'displayTrafficDataUsage'
|
||||||
|
| 'estimateTrafficDataCost'
|
||||||
| 'useMemoizedActiveTokens'
|
| 'useMemoizedActiveTokens'
|
||||||
| 'queryMissingTokens'
|
| 'queryMissingTokens'
|
||||||
| 'checkEdgeValidTokensFromCache'
|
| 'checkEdgeValidTokensFromCache'
|
||||||
@ -225,6 +226,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_DISPLAY_TRAFFIC_DATA_USAGE,
|
process.env.UNLEASH_EXPERIMENTAL_DISPLAY_TRAFFIC_DATA_USAGE,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
estimateTrafficDataCost: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_ESTIMATE_TRAFFIC_DATA_COST,
|
||||||
|
false,
|
||||||
|
),
|
||||||
userAccessUIEnabled: parseEnvVarBoolean(
|
userAccessUIEnabled: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_USER_ACCESS_UI_ENABLED,
|
process.env.UNLEASH_EXPERIMENTAL_USER_ACCESS_UI_ENABLED,
|
||||||
false,
|
false,
|
||||||
|
Loading…
Reference in New Issue
Block a user