mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-28 00:17:12 +01:00
feat(1-3267): use new API for chart creation (#9149)
Adds support for the new /traffic-search API behind a flag. When active, you'll be able to select month ranges as well as specific single months. Largely copies the existing network traffic component, and adds some minor tweaks to make it work with the new data. This is quite rough, but it gives us a base to build on for later. There's still things that we need to solve for in following PRs.
This commit is contained in:
parent
05e608ab09
commit
87a84426ec
@ -20,13 +20,17 @@ import {
|
||||
} from 'chart.js';
|
||||
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||
import {
|
||||
useInstanceTrafficMetrics,
|
||||
useInstanceTrafficMetrics2,
|
||||
} from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||
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 { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin';
|
||||
@ -36,6 +40,8 @@ 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';
|
||||
|
||||
const StyledBox = styled(Box)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
@ -148,10 +154,173 @@ const NewHeader = styled('div')(() => ({
|
||||
alignItems: 'flex-start',
|
||||
}));
|
||||
|
||||
export const NetworkTrafficUsage: FC = () => {
|
||||
const NewNetworkTrafficUsage: FC = () => {
|
||||
usePageTitle('Network - Data Usage');
|
||||
const theme = useTheme();
|
||||
|
||||
const { isOss } = useUiConfig();
|
||||
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const {
|
||||
record,
|
||||
newPeriod,
|
||||
setNewPeriod,
|
||||
toTrafficUsageSum,
|
||||
calculateOverageCost,
|
||||
calculateEstimatedMonthlyCost,
|
||||
} = useTrafficDataEstimation();
|
||||
|
||||
const includedTraffic = useTrafficLimit();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return createBarChartOptions(
|
||||
theme,
|
||||
(tooltipItems: any) => {
|
||||
if (newPeriod.grouping === 'daily') {
|
||||
const periodItem = record[newPeriod.month];
|
||||
const tooltipDate = new Date(
|
||||
periodItem.year,
|
||||
periodItem.month,
|
||||
Number.parseInt(tooltipItems[0].label),
|
||||
);
|
||||
return tooltipDate.toLocaleDateString(
|
||||
locationSettings?.locale ?? 'en-US',
|
||||
{
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return new Date(tooltipItems[0].label).toLocaleDateString(
|
||||
locationSettings?.locale ?? 'en-US',
|
||||
{
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
includedTraffic,
|
||||
);
|
||||
}, [theme, newPeriod]);
|
||||
|
||||
const traffic = useInstanceTrafficMetrics2(newPeriod);
|
||||
|
||||
const data = newToChartData(traffic.usage);
|
||||
|
||||
const [usageTotal, setUsageTotal] = useState<number>(0);
|
||||
|
||||
const [overageCost, setOverageCost] = useState<number>(0);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={isOss()}
|
||||
show={<Alert severity='warning'>Not enabled.</Alert>}
|
||||
elseShow={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={includedTraffic > 0 && 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>
|
||||
<NewHeader>
|
||||
{
|
||||
// todo: add new usage plan summary that works for monthly data as well as daily
|
||||
}
|
||||
|
||||
<NetworkTrafficUsagePlanSummary
|
||||
usageTotal={usageTotal}
|
||||
includedTraffic={includedTraffic}
|
||||
overageCost={overageCost}
|
||||
estimatedMonthlyCost={estimatedMonthlyCost}
|
||||
/>
|
||||
<PeriodSelector
|
||||
selectedPeriod={newPeriod}
|
||||
setPeriod={setNewPeriod}
|
||||
/>
|
||||
</NewHeader>
|
||||
<Bar
|
||||
data={data}
|
||||
plugins={[customHighlightPlugin()]} // todo: accomodate wide bars when grouping by month
|
||||
options={options}
|
||||
aria-label='An instance metrics line chart with two lines: requests per second for admin API and requests per second for client API' // todo: this isn't correct at all!
|
||||
/>
|
||||
</StyledBox>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const NetworkTrafficUsage: FC = () => {
|
||||
const useNewNetworkTraffic = useUiFlag('dataUsageMultiMonthView');
|
||||
return useNewNetworkTraffic ? (
|
||||
<NewNetworkTrafficUsage />
|
||||
) : (
|
||||
<OldNetworkTrafficUsage />
|
||||
);
|
||||
};
|
||||
|
||||
const OldNetworkTrafficUsage: FC = () => {
|
||||
usePageTitle('Network - Data Usage');
|
||||
const theme = useTheme();
|
||||
const showMultiMonthSelector = useUiFlag('dataUsageMultiMonthView');
|
||||
|
||||
const { isOss } = useUiConfig();
|
||||
|
||||
@ -279,49 +448,31 @@ export const NetworkTrafficUsage: FC = () => {
|
||||
}
|
||||
/>
|
||||
<StyledBox>
|
||||
{showMultiMonthSelector ? (
|
||||
<NewHeader>
|
||||
<Grid container component='header' spacing={2}>
|
||||
<Grid item xs={12} md={10}>
|
||||
<NetworkTrafficUsagePlanSummary
|
||||
usageTotal={usageTotal}
|
||||
includedTraffic={includedTraffic}
|
||||
overageCost={overageCost}
|
||||
estimatedMonthlyCost={estimatedMonthlyCost}
|
||||
/>
|
||||
<PeriodSelector
|
||||
selectedPeriod={period}
|
||||
setPeriod={setPeriod}
|
||||
/>
|
||||
</NewHeader>
|
||||
) : (
|
||||
<Grid container component='header' spacing={2}>
|
||||
<Grid item xs={12} md={10}>
|
||||
<NetworkTrafficUsagePlanSummary
|
||||
usageTotal={usageTotal}
|
||||
includedTraffic={includedTraffic}
|
||||
overageCost={overageCost}
|
||||
estimatedMonthlyCost={
|
||||
estimatedMonthlyCost
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={2}>
|
||||
<Select
|
||||
id='dataperiod-select'
|
||||
name='dataperiod'
|
||||
options={selectablePeriods}
|
||||
value={period}
|
||||
onChange={(e) =>
|
||||
setPeriod(e.target.value)
|
||||
}
|
||||
style={{
|
||||
minWidth: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
formControlStyles={{ width: '100%' }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} md={2}>
|
||||
<Select
|
||||
id='dataperiod-select'
|
||||
name='dataperiod'
|
||||
options={selectablePeriods}
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(e.target.value)}
|
||||
style={{
|
||||
minWidth: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
formControlStyles={{ width: '100%' }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={2}>
|
||||
<Bar
|
||||
data={data}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { type FC, useState } from 'react';
|
||||
import type { ChartDataSelection } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||
import type { FC } from 'react';
|
||||
|
||||
export type Period = {
|
||||
key: string;
|
||||
@ -147,25 +148,13 @@ type Selection =
|
||||
};
|
||||
|
||||
type Props = {
|
||||
selectedPeriod: string;
|
||||
setPeriod: (period: string) => void;
|
||||
selectedPeriod: ChartDataSelection;
|
||||
setPeriod: (period: ChartDataSelection) => void;
|
||||
};
|
||||
|
||||
export const PeriodSelector: FC<Props> = ({ selectedPeriod, setPeriod }) => {
|
||||
const selectablePeriods = getSelectablePeriods();
|
||||
|
||||
// this is for dev purposes; only to show how the design will work when you select a range.
|
||||
const [tempOverride, setTempOverride] = useState<Selection | null>();
|
||||
|
||||
const select = (value: Selection) => {
|
||||
if (value.type === 'month') {
|
||||
setTempOverride(null);
|
||||
setPeriod(value.value);
|
||||
} else {
|
||||
setTempOverride(value);
|
||||
}
|
||||
};
|
||||
|
||||
const rangeOptions = [3, 6, 12].map((monthsBack) => ({
|
||||
value: monthsBack,
|
||||
label: `Last ${monthsBack} months`,
|
||||
@ -183,17 +172,17 @@ export const PeriodSelector: FC<Props> = ({ selectedPeriod, setPeriod }) => {
|
||||
<li key={period.label}>
|
||||
<button
|
||||
className={
|
||||
!tempOverride &&
|
||||
period.key === selectedPeriod
|
||||
selectedPeriod.grouping === 'daily' &&
|
||||
period.key === selectedPeriod.month
|
||||
? 'selected'
|
||||
: ''
|
||||
}
|
||||
type='button'
|
||||
disabled={!period.selectable}
|
||||
onClick={() => {
|
||||
select({
|
||||
type: 'month',
|
||||
value: period.key,
|
||||
setPeriod({
|
||||
grouping: 'daily',
|
||||
month: period.key,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@ -211,16 +200,15 @@ export const PeriodSelector: FC<Props> = ({ selectedPeriod, setPeriod }) => {
|
||||
<li key={option.label}>
|
||||
<button
|
||||
className={
|
||||
tempOverride &&
|
||||
tempOverride.type === 'range' &&
|
||||
option.value === tempOverride.monthsBack
|
||||
selectedPeriod.grouping === 'monthly' &&
|
||||
option.value === selectedPeriod.monthsBack
|
||||
? 'selected'
|
||||
: ''
|
||||
}
|
||||
type='button'
|
||||
onClick={() => {
|
||||
select({
|
||||
type: 'range',
|
||||
setPeriod({
|
||||
grouping: 'monthly',
|
||||
monthsBack: option.value,
|
||||
});
|
||||
}}
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { format } from 'date-fns';
|
||||
import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi';
|
||||
|
||||
export const monthlyTrafficDataToCurrentUsage = (
|
||||
usage?: TrafficUsageDataSegmentedCombinedSchema,
|
||||
) => {
|
||||
if (!usage) {
|
||||
return 0;
|
||||
}
|
||||
const currentMonth = format(new Date(), 'yyyy-MM');
|
||||
return usage.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);
|
||||
};
|
@ -2,7 +2,12 @@ import useSWR from 'swr';
|
||||
import { useMemo } from 'react';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import type { TrafficUsageDataSegmentedSchema } from 'openapi';
|
||||
import type {
|
||||
TrafficUsageDataSegmentedCombinedSchema,
|
||||
TrafficUsageDataSegmentedCombinedSchemaApiDataItem,
|
||||
TrafficUsageDataSegmentedSchema,
|
||||
} from 'openapi';
|
||||
import { endOfMonth, format, startOfMonth, subMonths } from 'date-fns';
|
||||
|
||||
export interface IInstanceTrafficMetricsResponse {
|
||||
usage: TrafficUsageDataSegmentedSchema;
|
||||
@ -33,6 +38,64 @@ 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;
|
||||
|
||||
refetch: () => void;
|
||||
|
||||
loading: boolean;
|
||||
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export const useInstanceTrafficMetrics2 = (
|
||||
selection: ChartDataSelection,
|
||||
): InstanceTrafficMetricsResponse2 => {
|
||||
const { from, to } = fromSelection(selection);
|
||||
|
||||
const apiPath = `api/admin/metrics/traffic-search?grouping=${selection.grouping}&from=${from}&to=${to}`;
|
||||
|
||||
const { data, error, mutate } = useSWR(formatApiPath(apiPath), fetcher);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
usage: data,
|
||||
loading: !error && !data,
|
||||
refetch: () => mutate(),
|
||||
error,
|
||||
}),
|
||||
[data, error, mutate],
|
||||
);
|
||||
};
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Instance Metrics'))
|
||||
|
@ -1,6 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||
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;
|
||||
@ -104,6 +116,110 @@ const toPeriodsRecord = (
|
||||
);
|
||||
};
|
||||
|
||||
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) =>
|
||||
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,
|
||||
@ -236,11 +352,18 @@ export const useTrafficDataEstimation = () => {
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user