From b2abeff3b7a53b0bb974126197f8c9b07d7d4cbf Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 24 Jan 2025 11:45:26 +0100 Subject: [PATCH] feat(1-3267): use new API for chart creation wip --- .../NetworkTrafficUsage.tsx | 44 +++- .../useInstanceTrafficMetrics.ts | 84 ++++++ frontend/src/hooks/useTrafficData.ts | 239 ++++++++++++++++-- .../traffic-data-usage-store.test.ts | 1 + 4 files changed, 346 insertions(+), 22 deletions(-) diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 113ccd5926..6691b5448a 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -20,7 +20,10 @@ 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'; @@ -148,6 +151,21 @@ const NewHeader = styled('div')(() => ({ alignItems: 'flex-start', })); +const NewNetworkTrafficUsage: FC = () => { + usePageTitle('Network - Data Usage'); + const theme = useTheme(); + + const { isOss } = useUiConfig(); + + const selection = { format: 'daily' as const, month: '2025-01' }; + + const incoming = useInstanceTrafficMetrics2(selection); + + // do the mapping here somehow + + return
Network Traffic Usage
; +}; + export const NetworkTrafficUsage: FC = () => { usePageTitle('Network - Data Usage'); const theme = useTheme(); @@ -155,6 +173,8 @@ export const NetworkTrafficUsage: FC = () => { const { isOss } = useUiConfig(); + // what do we do here? if we're cutting the traffic usage box, might be best to split it into multiple components? + const { locationSettings } = useLocationSettings(); const { record, @@ -215,6 +235,15 @@ export const NetworkTrafficUsage: FC = () => { setDatasets(toChartData(labels, traffic, endpointsInfo)); }, [labels, traffic]); + // console.log( + // 'data', + // data, + // 'traffic', + // traffic, + // 'endpointsInfo', + // endpointsInfo, + // ); + useEffect(() => { if (record && period) { const periodData = record[period]; @@ -224,6 +253,7 @@ export const NetworkTrafficUsage: FC = () => { useEffect(() => { if (data) { + // if daily, there is a sum. if monthly, use the count from the current month const usage = toTrafficUsageSum(data.datasets); setUsageTotal(usage); if (includedTraffic > 0) { @@ -247,6 +277,18 @@ export const NetworkTrafficUsage: FC = () => { } }, [data]); + // if single month: + // overage warning + // num requests to unleash this month + // selector + // chart + // + // if multi month: + // overage warning + // avg requests per month for preceding n months + // selector + // char + return ( { + const fmt = (date: Date) => format(date, 'YYYY-MM-dd'); + if (selection.format === '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 SegmentedSchema = { + format: 'monthly' | 'daily'; + period: { from: string; to: string }; + apiData: [ + { + apiPath: string; + dataPoints: [ + { + // other options: period? time? interval? for? + when: string; // in API: string formatted as full date or YYYY-MM, depending on monthly/daily + trafficTypes: [ + { + group: string; // we could do 'successful-requests', but that might constrain us in the future + count: number; // natural number + }, + ]; + }, + ]; + }, + ]; +}; + +export type SegmentedSchemaApiData = SegmentedSchema['apiData'][number]; + +export type InstanceTrafficMetricsResponse2 = { + usage: SegmentedSchema; + + refetch: () => void; + + loading: boolean; + + error?: Error; +}; + +export const useInstanceTrafficMetrics2 = ( + selection: Selection, +): InstanceTrafficMetricsResponse2 => { + const { from, to } = fromSelection(selection); + console.log('would use these from and to dates', from, to); + + const apiPath = + selection.format === 'daily' + ? `api/admin/metrics/traffic2?format=daily&month=${selection.month}` + : `api/admin/metrics/traffic2?format=monthly&monthsBack=${selection.monthsBack}`; + + 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')) diff --git a/frontend/src/hooks/useTrafficData.ts b/frontend/src/hooks/useTrafficData.ts index b2aefe0598..f47ec98a98 100644 --- a/frontend/src/hooks/useTrafficData.ts +++ b/frontend/src/hooks/useTrafficData.ts @@ -1,6 +1,17 @@ import { useState } from 'react'; -import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import type { + IInstanceTrafficMetricsResponse, + InstanceTrafficMetricsResponse2, + SegmentedSchemaApiData, +} from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import type { ChartDataset } from 'chart.js'; +import { + addDays, + addMonths, + differenceInCalendarDays, + differenceInCalendarMonths, + format, +} from 'date-fns'; const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5; const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000; @@ -104,36 +115,145 @@ const toPeriodsRecord = ( ); }; -const toChartData = ( - days: number[], - traffic: IInstanceTrafficMetricsResponse, +const toMonthlyChartData = ( + traffic: InstanceTrafficMetricsResponse2, endpointsInfo: Record, -): ChartDatasetType[] => { +): { datasets: ChartDatasetType[]; labels: string[] } => { if (!traffic || !traffic.usage || !traffic.usage.apiData) { - return []; + return { labels: [], datasets: [] }; } - const data = traffic.usage.apiData + const from = new Date(traffic.usage.period.from); + const to = new Date(traffic.usage.period.to); + const numMonths = Math.abs(differenceInCalendarMonths(to, from)); + const formatMonth = (date: Date) => format(date, 'yyyy-MM'); + + const datasets = traffic.usage.apiData .filter((item) => !!endpointsInfo[item.apiPath]) .sort( - (item1: any, item2: any) => + (item1: SegmentedSchemaApiData, item2: SegmentedSchemaApiData) => 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, - ); - - for (const dayKey in item.days) { - const day = item.days[dayKey]; - const dayNum = new Date(Date.parse(day.day)).getUTCDate(); - daysRec[`d${dayNum}`] = day.trafficTypes[0].count; + .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.when] = 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 + 1 }).map((_, index) => + formatMonth(addMonths(from, index)), + ); + + return { datasets, labels }; +}; + +// const getDailyChartDataRec = (period: { from: string; to: string }) => { +// const from = new Date(period.from); +// const to = new Date(period.to); +// const numDays = Math.abs(differenceInCalendarDays(to, from)); +// 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; +// } + +// return () => ({ +// ...daysRec, +// }); +// }; + +// const toAnyChartData = +// (getDataRec: () => { [key: string]: number }) => +// ( +// traffic: InstanceTrafficMetricsResponse2, +// endpointsInfo: Record, +// ): ChartDatasetType[] => { +// if (!traffic || !traffic.usage || !traffic.usage.apiData) { +// return []; +// } + +// const data = traffic.usage.apiData +// .filter((item) => !!endpointsInfo[item.apiPath]) +// .sort( +// ( +// item1: SegmentedSchemaApiData, +// item2: SegmentedSchemaApiData, +// ) => +// endpointsInfo[item1.apiPath].order - +// endpointsInfo[item2.apiPath].order, +// ) +// .map((item: SegmentedSchemaApiData) => { +// const entries = getDataRec(); + +// for (const day of Object.values(item.dataPoints)) { +// entries[day.when] = day.trafficTypes[0].count; +// } + +// const epInfo = endpointsInfo[item.apiPath]; + +// return { +// label: epInfo.label, +// data: Object.values(entries), +// backgroundColor: epInfo.color, +// hoverBackgroundColor: epInfo.color, +// }; +// }); + +// return data; +// }; + +const toDailyChartData = ( + traffic: InstanceTrafficMetricsResponse2, + endpointsInfo: Record, +): { datasets: ChartDatasetType[]; labels: number[] } => { + if (!traffic || !traffic.usage || !traffic.usage.apiData) { + return { datasets: [], labels: [] }; + } + + const from = new Date(traffic.usage.period.from); + const to = new Date(traffic.usage.period.to); + const numDays = Math.abs(differenceInCalendarDays(to, from)); + 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 = traffic.usage.apiData + .filter((item) => !!endpointsInfo[item.apiPath]) + .sort( + (item1: SegmentedSchemaApiData, item2: SegmentedSchemaApiData) => + endpointsInfo[item1.apiPath].order - + endpointsInfo[item2.apiPath].order, + ) + .map((item: SegmentedSchemaApiData) => { + const daysRec = getDaysRec(); + + for (const day of Object.values(item.dataPoints)) { + daysRec[day.when] = day.trafficTypes[0].count; + } + const epInfo = endpointsInfo[item.apiPath]; return { @@ -144,6 +264,72 @@ const toChartData = ( }; }); + // simplification: assumings 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, + endpointsInfo: Record, +): ChartDatasetType[] => { + if (!traffic || !traffic.usage || !traffic.usage.apiData) { + return []; + } + + // days contains all the days of the month because the usage data may not have entries for all days. so it + + const data = traffic.usage.apiData + .filter((item) => !!endpointsInfo[item.apiPath]) // ignore /edge and unknown endpoints + .sort( + // sort the data such that admin goes before frontend goes before client + (item1: any, item2: any) => + endpointsInfo[item1.apiPath].order - + endpointsInfo[item2.apiPath].order, + ) + .map((item: any) => { + // generate a list of 0s for each day of the month + const daysRec = days.reduce( + (acc, day: number) => { + acc[`d${day}`] = 0; + return acc; + }, + {} as Record, + ); + + console.log(item, daysRec); + + // for each day in the usage data + for (const dayKey in item.days) { + const day = item.days[dayKey]; + // get the day of the month (probably don't need the Date parse) + const dayNum = new Date(Date.parse(day.day)).getUTCDate(); + // add the count to the record for that day + daysRec[`d${dayNum}`] = day.trafficTypes[0].count; + } + const epInfo = endpointsInfo[item.apiPath]; + + console.log(daysRec, Object.values(daysRec)); + return { + label: epInfo.label, + // traversal order is well-defined + data: Object.values(daysRec), + backgroundColor: epInfo.color, + hoverBackgroundColor: epInfo.color, + }; + }); + + console.log( + 'traffic data to chart data', + days, + traffic.usage, + endpointsInfo, + 'result:', + data, + ); + return data; }; @@ -234,6 +420,17 @@ export const calculateEstimatedMonthlyCost = ( export const useTrafficDataEstimation = () => { const selectablePeriods = getSelectablePeriods(); const record = toPeriodsRecord(selectablePeriods); + console.log('RECORD', record); // Contains each month of the past year: + // { + // // ... other props + // "2024-12": { + // "key": "2024-12", + // "year": 2024, + // "month": 11, + // "dayCount": 31, + // "label": "December 2024" + // } + // } const [period, setPeriod] = useState(selectablePeriods[0].key); return { diff --git a/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts b/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts index 3d40f19c40..ea1953e802 100644 --- a/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts +++ b/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts @@ -237,6 +237,7 @@ test('can query for monthly aggregation of data for a specified range', async () const result = await trafficDataUsageStore.getTrafficDataForMonthRange(monthsBack); + console.log(result); // should have the current month and the preceding n months (one entry per group) expect(result.length).toBe((monthsBack + 1) * 2);