diff --git a/frontend/src/component/admin/network/Network.tsx b/frontend/src/component/admin/network/Network.tsx index cd82e8f547..0943df226e 100644 --- a/frontend/src/component/admin/network/Network.tsx +++ b/frontend/src/component/admin/network/Network.tsx @@ -14,6 +14,9 @@ const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic')); const NetworkTrafficUsage = lazy( () => import('./NetworkTrafficUsage/NetworkTrafficUsage'), ); +const BackendConnections = lazy( + () => import('./NetworkTrafficUsage/BackendConnections'), +); const tabs = [ { @@ -28,17 +31,31 @@ const tabs = [ label: 'Connected Edges', path: '/admin/network/connected-edges', }, +]; + +const seatModelTabs = [ { label: 'Data Usage', path: '/admin/network/data-usage', }, ]; +const consumptionModelTabs = [ + { + label: 'Backend Connections', + path: '/admin/network/backend-connections', + }, +]; + export const Network = () => { const { pathname } = useLocation(); const edgeObservabilityEnabled = useUiFlag('edgeObservability'); + const consumptionModelEnabled = useUiFlag('consumptionModel'); + const allTabs = consumptionModelEnabled + ? [...tabs, ...consumptionModelTabs] + : [...tabs, ...seatModelTabs]; - const filteredTabs = tabs.filter( + const filteredTabs = allTabs.filter( ({ label }) => label !== 'Connected Edges' || edgeObservabilityEnabled, ); @@ -82,6 +99,10 @@ export const Network = () => { path='data-usage' element={} /> + } + /> diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/BackendConnections.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/BackendConnections.tsx new file mode 100644 index 0000000000..2c0c4dfdfb --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/BackendConnections.tsx @@ -0,0 +1,75 @@ +import type { FC } from 'react'; +import { usePageTitle } from 'hooks/usePageTitle'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Alert, Box } from '@mui/material'; +import { PeriodSelector } from './PeriodSelector'; +import { Bar } from 'react-chartjs-2'; +import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin'; +import { getChartLabel } from './chart-functions'; +import { useConsumptionStats } from './hooks/useStats'; +import { StyledBox, TopRow } from './SharedComponents'; +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + Title, + Tooltip, +} from 'chart.js'; +import annotationPlugin from 'chartjs-plugin-annotation'; +import { useChartDataSelection } from './hooks/useChartDataSelection'; + +export const BackendConnections: FC = () => { + usePageTitle('Network - Backend Connections'); + + const { isOss } = useUiConfig(); + + const { chartDataSelection, setChartDataSelection, options } = + useChartDataSelection(); + + const { chartData } = useConsumptionStats(chartDataSelection); + + return ( + Not enabled.} + elseShow={ + <> + + + + 1 connection = 7200 backend SDK requests per day + + + + + + + } + /> + ); +}; + +// Register dependencies that we need to draw the chart. +ChartJS.register( + annotationPlugin, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +); + +// Use a default export to lazy-load the charting library. +export default BackendConnections; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 54d931d54a..1484df2426 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -1,172 +1,45 @@ -import { useMemo, useState, useEffect, type FC } from 'react'; +import { type FC, useEffect, useMemo, useState } from 'react'; import useTheme from '@mui/material/styles/useTheme'; import styled from '@mui/material/styles/styled'; import { usePageTitle } from 'hooks/usePageTitle'; import Select from 'component/common/select'; -import Box from '@mui/system/Box'; import { Link as RouterLink } from 'react-router-dom'; import { Alert, Link } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { - Chart as ChartJS, - type ChartOptions, - CategoryScale, - LinearScale, BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, Title, Tooltip, - Legend, } from 'chart.js'; import { Bar } from 'react-chartjs-2'; -import { - useInstanceTrafficMetrics, - useTrafficSearch, -} from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; -import type { Theme } from '@mui/material/styles/createTheme'; +import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import Grid from '@mui/material/Grid'; import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary'; import annotationPlugin from 'chartjs-plugin-annotation'; import { - useTrafficDataEstimation, calculateEstimatedMonthlyCost as deprecatedCalculateEstimatedMonthlyCost, + useTrafficDataEstimation, } from 'hooks/useTrafficData'; import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin'; -import { formatTickValue } from 'component/common/Chart/formatTickValue'; import { useTrafficLimit } from './hooks/useTrafficLimit'; import { BILLING_TRAFFIC_BUNDLE_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan'; import { useLocationSettings } from 'hooks/useLocationSettings'; import { PeriodSelector } from './PeriodSelector'; import { useUiFlag } from 'hooks/useUiFlag'; 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 { type ChartDataSelection, toDateRange } from './chart-data-selection'; -import { - type ChartDatasetType, - getChartLabel, - toChartData as newToChartData, - toConnectionChartData, -} from './chart-functions'; -import { periodsRecord, selectablePeriods } from './selectable-periods'; - -const StyledBox = styled(Box)(({ theme }) => ({ - display: 'grid', - gap: theme.spacing(5), -})); - -const createBarChartOptions = ( - theme: Theme, - tooltipTitleCallback: (tooltipItems: any) => string, - includedTraffic?: number, -): ChartOptions<'bar'> => ({ - plugins: { - annotation: { - clip: false, - annotations: { - line: { - type: 'line', - borderDash: [5, 5], - yMin: includedTraffic ? includedTraffic / 30 : 0, - yMax: includedTraffic ? includedTraffic / 30 : 0, - borderColor: 'gray', - borderWidth: 1, - display: !!includedTraffic, - - label: { - backgroundColor: 'rgba(192, 192, 192, 0.8)', - color: 'black', - padding: { - top: 10, - bottom: 10, - left: 10, - right: 10, - }, - content: 'Average daily requests included in your plan', - display: !!includedTraffic, - }, - }, - }, - }, - legend: { - position: 'bottom', - labels: { - color: theme.palette.text.primary, - pointStyle: 'circle', - usePointStyle: true, - boxHeight: 6, - padding: 15, - boxPadding: 5, - }, - }, - tooltip: { - backgroundColor: theme.palette.background.paper, - titleColor: theme.palette.text.primary, - bodyColor: theme.palette.text.primary, - bodySpacing: 6, - padding: { - top: 20, - bottom: 20, - left: 30, - right: 30, - }, - borderColor: 'rgba(0, 0, 0, 0.05)', - borderWidth: 3, - usePointStyle: true, - caretSize: 0, - boxPadding: 10, - callbacks: { - title: tooltipTitleCallback, - }, - }, - }, - responsive: true, - scales: { - x: { - stacked: true, - ticks: { - color: theme.palette.text.secondary, - }, - grid: { - display: false, - }, - }, - y: { - stacked: true, - ticks: { - color: theme.palette.text.secondary, - maxTicksLimit: 5, - callback: formatTickValue, - }, - grid: { - drawBorder: false, - }, - }, - }, - elements: { - bar: { - borderRadius: 5, - }, - }, - interaction: { - mode: 'index', - intersect: false, - }, -}); - -const TopRow = styled('div')(({ theme }) => ({ - display: 'flex', - flexFlow: 'row wrap', - justifyContent: 'space-between', - gap: theme.spacing(2, 4), - alignItems: 'start', -})); +import { calculateOverageCost } from 'utils/traffic-calculations'; +import { currentMonth } from './dates'; +import { type ChartDatasetType, getChartLabel } from './chart-functions'; +import { createBarChartOptions } from './bar-chart-options'; +import { useTrafficStats } from './hooks/useStats'; +import { BoldText, StyledBox, TopRow } from './SharedComponents'; +import { useChartDataSelection } from './hooks/useChartDataSelection'; const TrafficInfoBoxes = styled('div')(({ theme }) => ({ display: 'grid', @@ -175,123 +48,17 @@ const TrafficInfoBoxes = styled('div')(({ theme }) => ({ gap: theme.spacing(2, 4), })); -const BoldText = styled('span')(({ theme }) => ({ - fontWeight: 'bold', -})); - -const useTrafficStats = ( - includedTraffic: number, - chartDataSelection: ChartDataSelection, -) => { - const consumptionModelEnabled = useUiFlag('consumptionModel'); - const { result } = useTrafficSearch( - chartDataSelection.grouping, - toDateRange(chartDataSelection, currentDate), - ); - const results = useMemo(() => { - if (result.state !== 'success') { - return { - chartData: { datasets: [], labels: [] }, - usageTotal: 0, - overageCost: 0, - estimatedMonthlyCost: 0, - requestSummaryUsage: 0, - }; - } - const traffic = result.data; - - const chartData = consumptionModelEnabled - ? toConnectionChartData(traffic) - : newToChartData(traffic); - const usageTotal = calculateTotalUsage(traffic); - const overageCost = calculateOverageCost( - usageTotal, - includedTraffic, - BILLING_TRAFFIC_BUNDLE_PRICE, - ); - - const estimatedMonthlyCost = calculateEstimatedMonthlyCost( - traffic.apiData, - includedTraffic, - currentDate, - BILLING_TRAFFIC_BUNDLE_PRICE, - ); - - const requestSummaryUsage = - chartDataSelection.grouping === 'daily' - ? usageTotal - : averageTrafficPreviousMonths(traffic); - - return { - chartData, - usageTotal, - overageCost, - estimatedMonthlyCost, - requestSummaryUsage, - }; - }, [ - JSON.stringify(result), - includedTraffic, - JSON.stringify(chartDataSelection), - ]); - - return results; -}; - const NewNetworkTrafficUsage: FC = () => { usePageTitle('Network - Data Usage'); - const theme = useTheme(); const estimateTrafficDataCost = useUiFlag('estimateTrafficDataCost'); const { isOss } = useUiConfig(); - const { locationSettings } = useLocationSettings(); - - const [chartDataSelection, setChartDataSelection] = - useState({ - grouping: 'daily', - month: selectablePeriods[0].key, - }); - const includedTraffic = useTrafficLimit(); - const options = useMemo(() => { - return createBarChartOptions( - theme, - (tooltipItems: any) => { - if (chartDataSelection.grouping === 'daily') { - const periodItem = periodsRecord[chartDataSelection.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 { - const timestamp = Date.parse(tooltipItems[0].label); - if (Number.isNaN(timestamp)) { - return 'Current month to date'; - } - return new Date(timestamp).toLocaleDateString( - locationSettings?.locale ?? 'en-US', - { - month: 'long', - year: 'numeric', - }, - ); - } - }, - includedTraffic, - ); - }, [theme, chartDataSelection]); + const { chartDataSelection, setChartDataSelection, options } = + useChartDataSelection(includedTraffic); const { chartData, diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/SharedComponents.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/SharedComponents.tsx new file mode 100644 index 0000000000..a6f29c97cf --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/SharedComponents.tsx @@ -0,0 +1,19 @@ +import styled from '@mui/material/styles/styled'; +import Box from '@mui/system/Box'; + +export const StyledBox = styled(Box)(({ theme }) => ({ + display: 'grid', + gap: theme.spacing(5), +})); + +export const TopRow = styled('div')(({ theme }) => ({ + display: 'flex', + flexFlow: 'row wrap', + justifyContent: 'space-between', + gap: theme.spacing(2, 4), + alignItems: 'start', +})); + +export const BoldText = styled('span')(({ theme }) => ({ + fontWeight: 'bold', +})); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/bar-chart-options.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/bar-chart-options.ts new file mode 100644 index 0000000000..2f09ab5d23 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/bar-chart-options.ts @@ -0,0 +1,102 @@ +import type { Theme } from '@mui/material/styles/createTheme'; +import type { ChartOptions } from 'chart.js'; +import { formatTickValue } from 'component/common/Chart/formatTickValue'; + +export const createBarChartOptions = ( + theme: Theme, + tooltipTitleCallback: (tooltipItems: any) => string, + includedTraffic?: number, +): ChartOptions<'bar'> => ({ + plugins: { + annotation: { + clip: false, + annotations: { + line: { + type: 'line', + borderDash: [5, 5], + yMin: includedTraffic ? includedTraffic / 30 : 0, + yMax: includedTraffic ? includedTraffic / 30 : 0, + borderColor: 'gray', + borderWidth: 1, + display: !!includedTraffic, + + label: { + backgroundColor: 'rgba(192, 192, 192, 0.8)', + color: 'black', + padding: { + top: 10, + bottom: 10, + left: 10, + right: 10, + }, + content: 'Average daily requests included in your plan', + display: !!includedTraffic, + }, + }, + }, + }, + legend: { + position: 'bottom', + labels: { + color: theme.palette.text.primary, + pointStyle: 'circle', + usePointStyle: true, + boxHeight: 6, + padding: 15, + boxPadding: 5, + }, + }, + tooltip: { + backgroundColor: theme.palette.background.paper, + titleColor: theme.palette.text.primary, + bodyColor: theme.palette.text.primary, + bodySpacing: 6, + padding: { + top: 20, + bottom: 20, + left: 30, + right: 30, + }, + borderColor: 'rgba(0, 0, 0, 0.05)', + borderWidth: 3, + usePointStyle: true, + caretSize: 0, + boxPadding: 10, + callbacks: { + title: tooltipTitleCallback, + }, + }, + }, + responsive: true, + scales: { + x: { + stacked: true, + ticks: { + color: theme.palette.text.secondary, + }, + grid: { + display: false, + }, + }, + y: { + stacked: true, + ticks: { + color: theme.palette.text.secondary, + maxTicksLimit: 5, + callback: formatTickValue, + }, + grid: { + drawBorder: false, + }, + }, + }, + elements: { + bar: { + borderRadius: 5, + }, + }, + interaction: { + mode: 'index', + intersect: false, + }, +}); diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.test.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.test.ts index ddcd3e8fc0..097f9988fa 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.test.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.test.ts @@ -1,5 +1,5 @@ import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi'; -import { toChartData } from './chart-functions'; +import { toTrafficUsageChartData } from './chart-functions'; import { endpointsInfo } from './endpoint-info'; describe('toChartData', () => { @@ -67,7 +67,7 @@ describe('toChartData', () => { ], }; - expect(toChartData(input)).toMatchObject(expectedOutput); + expect(toTrafficUsageChartData(input)).toMatchObject(expectedOutput); }); test('daily data conversion', () => { @@ -121,7 +121,7 @@ describe('toChartData', () => { ), }; - expect(toChartData(input)).toMatchObject(expectedOutput); + expect(toTrafficUsageChartData(input)).toMatchObject(expectedOutput); }); test('sorts endpoints according to endpoint data spec', () => { @@ -146,6 +146,6 @@ describe('toChartData', () => { ], }; - expect(toChartData(input)).toMatchObject(expectedOutput); + expect(toTrafficUsageChartData(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 44c86a9eeb..1ba2a98fce 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts @@ -13,7 +13,7 @@ import { formatDay, formatMonth } from './dates'; import type { ChartDataSelection } from './chart-data-selection'; export type ChartDatasetType = ChartDataset<'bar'>; -export const toChartData = ( +export const toTrafficUsageChartData = ( traffic: TrafficUsageDataSegmentedCombinedSchema, ): { datasets: ChartDatasetType[]; labels: string[] } => { const { newRecord, labels } = getLabelsAndRecords(traffic); @@ -47,6 +47,7 @@ export const toConnectionChartData = ( ): { datasets: ChartDatasetType[]; labels: string[] } => { const { newRecord, labels } = getLabelsAndRecords(traffic); const datasets = traffic.apiData + .filter((apiData) => apiData.apiPath === '/api/client') .sort( (item1, item2) => endpointsInfo[item1.apiPath].order - @@ -61,11 +62,14 @@ export const toConnectionChartData = ( if (traffic.grouping === 'monthly') { // 1 connections = 7200 * days in month requests per day const daysInMonth = getDaysInMonth(date); - record[dataPoint.period] = - requestCount / (daysInMonth * 7200); + record[dataPoint.period] = Number( + (requestCount / (daysInMonth * 7200)).toFixed(1), + ); } else { // 1 connection = 7200 requests per day - record[dataPoint.period] = requestCount / 7200; + record[dataPoint.period] = Number( + (requestCount / 7200).toFixed(1), + ); } } diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts new file mode 100644 index 0000000000..9b6618809f --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts @@ -0,0 +1,56 @@ +import { useMemo, useState } from 'react'; +import type { ChartDataSelection } from '../chart-data-selection'; +import { periodsRecord, selectablePeriods } from '../selectable-periods'; +import { createBarChartOptions } from '../bar-chart-options'; +import useTheme from '@mui/material/styles/useTheme'; +import { useLocationSettings } from 'hooks/useLocationSettings'; + +export const useChartDataSelection = (includedTraffic?: number) => { + const theme = useTheme(); + const { locationSettings } = useLocationSettings(); + + const [chartDataSelection, setChartDataSelection] = + useState({ + grouping: 'daily', + month: selectablePeriods[0].key, + }); + + const options = useMemo(() => { + return createBarChartOptions( + theme, + (tooltipItems: any) => { + if (chartDataSelection.grouping === 'daily') { + const periodItem = periodsRecord[chartDataSelection.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 { + const timestamp = Date.parse(tooltipItems[0].label); + if (Number.isNaN(timestamp)) { + return 'Current month to date'; + } + return new Date(timestamp).toLocaleDateString( + locationSettings?.locale ?? 'en-US', + { + month: 'long', + year: 'numeric', + }, + ); + } + }, + includedTraffic, + ); + }, [theme, chartDataSelection]); + + return { chartDataSelection, setChartDataSelection, options }; +}; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts new file mode 100644 index 0000000000..1fc29c5335 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts @@ -0,0 +1,98 @@ +import { type ChartDataSelection, toDateRange } from '../chart-data-selection'; +import { useTrafficSearch } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import { currentDate } from '../dates'; +import { useMemo } from 'react'; +import { + toTrafficUsageChartData as newToChartData, + toConnectionChartData, +} from '../chart-functions'; +import { + calculateEstimatedMonthlyCost, + calculateOverageCost, + calculateTotalUsage, +} from 'utils/traffic-calculations'; +import { BILLING_TRAFFIC_BUNDLE_PRICE } from '../../../billing/BillingDashboard/BillingPlan/BillingPlan'; +import { averageTrafficPreviousMonths } from '../average-traffic-previous-months'; + +export const useTrafficStats = ( + includedTraffic: number, + chartDataSelection: ChartDataSelection, +) => { + const { result } = useTrafficSearch( + chartDataSelection.grouping, + toDateRange(chartDataSelection, currentDate), + ); + const results = useMemo(() => { + if (result.state !== 'success') { + return { + chartData: { datasets: [], labels: [] }, + usageTotal: 0, + overageCost: 0, + estimatedMonthlyCost: 0, + requestSummaryUsage: 0, + }; + } + const traffic = result.data; + + const chartData = newToChartData(traffic); + const usageTotal = calculateTotalUsage(traffic); + const overageCost = calculateOverageCost( + usageTotal, + includedTraffic, + BILLING_TRAFFIC_BUNDLE_PRICE, + ); + + const estimatedMonthlyCost = calculateEstimatedMonthlyCost( + traffic.apiData, + includedTraffic, + currentDate, + BILLING_TRAFFIC_BUNDLE_PRICE, + ); + + const requestSummaryUsage = + chartDataSelection.grouping === 'daily' + ? usageTotal + : averageTrafficPreviousMonths(traffic); + + return { + chartData, + usageTotal, + overageCost, + estimatedMonthlyCost, + requestSummaryUsage, + }; + }, [ + JSON.stringify(result), + includedTraffic, + JSON.stringify(chartDataSelection), + ]); + + return results; +}; + +export const useConsumptionStats = (chartDataSelection: ChartDataSelection) => { + const { result } = useTrafficSearch( + chartDataSelection.grouping, + toDateRange(chartDataSelection, currentDate), + ); + const results = useMemo(() => { + if (result.state !== 'success') { + return { + chartData: { datasets: [], labels: [] }, + usageTotal: 0, + overageCost: 0, + estimatedMonthlyCost: 0, + requestSummaryUsage: 0, + }; + } + const traffic = result.data; + + const chartData = toConnectionChartData(traffic); + + return { + chartData, + }; + }, [JSON.stringify(result), JSON.stringify(chartDataSelection)]); + + return results; +};