diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/FrontendNetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/FrontendNetworkTrafficUsage.tsx index 44926c6d8a..a4e523571b 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/FrontendNetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/FrontendNetworkTrafficUsage.tsx @@ -18,7 +18,7 @@ import annotationPlugin from 'chartjs-plugin-annotation'; import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin'; import { PeriodSelector } from './PeriodSelector'; import { getChartLabel } from './chart-functions'; -import { useTrafficStats } from './hooks/useStats'; +import { useRequestsStats } from './hooks/useStats'; import { StyledBox, TopRow } from './SharedComponents'; import { useChartDataSelection } from './hooks/useChartDataSelection'; @@ -30,12 +30,7 @@ const FrontendNetworkTrafficUsage: FC = () => { const { chartDataSelection, setChartDataSelection, options } = useChartDataSelection(); - const includedTraffic = 0; - const { chartData } = useTrafficStats( - includedTraffic, - chartDataSelection, - '/api/frontend', - ); + const { chartData } = useRequestsStats(chartDataSelection); return ( { expect(toConnectionChartData(input)).toMatchObject(expectedOutput); }); }); + +describe('toRequestChartData', () => { + const dataPoint = (period: string, requests: number) => ({ + period, + requests, + }); + + test('monthly data conversion', () => { + const input: MeteredRequestsSchema = { + grouping: 'monthly', + dateRange: { + from: '2025-01-01', + to: '2025-06-30', + }, + apiData: [ + { + meteredGroup: 'default', + dataPoints: [ + dataPoint('2025-06', 15), + dataPoint('2025-01', 9), + dataPoint('2025-03', 14), + dataPoint('2025-04', 18), + ], + }, + ], + }; + + const expectedOutput = { + datasets: [ + { + data: [9, 0, 14, 18, 0, 15], + hoverBackgroundColor: '#A39EFF', + label: 'Frontend requests', + backgroundColor: '#A39EFF', + }, + ], + labels: [ + '2025-01', + '2025-02', + '2025-03', + '2025-04', + '2025-05', + 'Current month', + ], + }; + + expect(toRequestChartData(input)).toMatchObject(expectedOutput); + }); + + test('daily data conversion', () => { + const input: MeteredRequestsSchema = { + grouping: 'daily', + dateRange: { + from: '2025-01-01', + to: '2025-01-31', + }, + apiData: [ + { + meteredGroup: 'default', + dataPoints: [ + dataPoint('2025-01-02', 3), + dataPoint('2025-01-17', 7), + dataPoint('2025-01-19', 5), + dataPoint('2025-01-06', 10), + ], + }, + ], + }; + + const expectedOutput = { + datasets: [ + { + data: [ + 0, 3, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, + 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + hoverBackgroundColor: '#A39EFF', + label: 'Frontend requests', + backgroundColor: '#A39EFF', + }, + ], + labels: Array.from({ length: 31 }).map((_, index) => + (index + 1).toString(), + ), + }; + + expect(toRequestChartData(input)).toMatchObject(expectedOutput); + }); +}); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts index 202d5b4c07..f001b313b0 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts @@ -1,6 +1,7 @@ import type { ChartDataset } from 'chart.js'; import type { MeteredConnectionsSchema, + MeteredRequestsSchema, TrafficUsageDataSegmentedCombinedSchema, } from 'openapi'; import { endpointsInfo } from './endpoint-info'; @@ -73,6 +74,34 @@ export const toConnectionChartData = ( return { datasets, labels }; }; +export const toRequestChartData = ( + traffic: MeteredRequestsSchema, +): { datasets: ChartDatasetType[]; labels: string[] } => { + const { newRecord, labels } = getLabelsAndRecords(traffic); + const datasets = traffic.apiData.map((item) => { + const record = newRecord(); + for (const dataPoint of Object.values(item.dataPoints)) { + const requestCount = dataPoint.requests; + record[dataPoint.period] = requestCount; + } + + const epInfo = { + label: 'Frontend requests', + color: '#A39EFF', + order: 1, + }; + + return { + label: epInfo.label, + data: Object.values(record), + backgroundColor: epInfo.color, + hoverBackgroundColor: epInfo.color, + }; + }); + + return { datasets, labels }; +}; + const getLabelsAndRecords = ( traffic: Pick< TrafficUsageDataSegmentedCombinedSchema, diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts index 805cea2fad..c34f4ab910 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts @@ -4,6 +4,7 @@ import { currentDate } from '../dates'; import { useMemo } from 'react'; import { toConnectionChartData, + toRequestChartData, toTrafficUsageChartData, } from '../chart-functions'; import { @@ -14,6 +15,7 @@ import { import { BILLING_TRAFFIC_BUNDLE_PRICE } from '../../../billing/BillingDashboard/BillingPlan/BillingPlan'; import { averageTrafficPreviousMonths } from '../average-traffic-previous-months'; import { useConnectionsConsumption } from 'hooks/api/getters/useConnectionsConsumption/useConnectionsConsumption'; +import { useRequestsConsumption } from 'hooks/api/getters/useRequestsConsumption/useRequestsConsumption'; export const useTrafficStats = ( includedTraffic: number, @@ -94,3 +96,26 @@ export const useConsumptionStats = (chartDataSelection: ChartDataSelection) => { return results; }; + +export const useRequestsStats = (chartDataSelection: ChartDataSelection) => { + const { result } = useRequestsConsumption( + chartDataSelection.grouping, + toDateRange(chartDataSelection, currentDate), + ); + const results = useMemo(() => { + if (result.state !== 'success') { + return { + chartData: { datasets: [], labels: [] }, + }; + } + const traffic = result.data; + + const chartData = toRequestChartData(traffic); + + return { + chartData, + }; + }, [JSON.stringify(result), JSON.stringify(chartDataSelection)]); + + return results; +}; diff --git a/frontend/src/hooks/api/getters/useRequestsConsumption/useRequestsConsumption.ts b/frontend/src/hooks/api/getters/useRequestsConsumption/useRequestsConsumption.ts new file mode 100644 index 0000000000..030956539a --- /dev/null +++ b/frontend/src/hooks/api/getters/useRequestsConsumption/useRequestsConsumption.ts @@ -0,0 +1,46 @@ +import useSWR from 'swr'; +import { useMemo } from 'react'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import type { MeteredRequestsSchema } from 'openapi'; + +export type MeteredRequestsResponse = { + refetch: () => void; + result: + | { state: 'success'; data: MeteredRequestsSchema } + | { state: 'error'; error: Error } + | { state: 'loading' }; +}; + +export const useRequestsConsumption = ( + grouping: 'monthly' | 'daily', + { + from, + to, + }: { + from: string; + to: string; + }, +): MeteredRequestsResponse => { + const apiPath = `api/admin/metrics/request?grouping=${grouping}&from=${from}&to=${to}`; + + const { data, error, mutate } = useSWR(formatApiPath(apiPath), fetcher); + + return useMemo(() => { + const result = data + ? { state: 'success' as const, data: data } + : error + ? { state: 'error' as const, error } + : { state: 'loading' as const }; + return { + refetch: () => mutate(), + result, + }; + }, [data, error, mutate]); +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('Consumption Requests Metrics')) + .then((res) => res.json()); +}; diff --git a/website/sidebars.ts b/website/sidebars.ts index 907976a760..8c9771f694 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -135,7 +135,7 @@ const sidebars: SidebarsConfig = { type: 'doc', label: 'Security and Compliance', id: 'feature-flag-tutorials/use-cases/security-compliance', - } + }, ], }, {