1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-28 00:17:12 +01:00

feat: backend connections tab (#9381)

This commit is contained in:
Mateusz Kwasniewski 2025-02-27 13:38:42 +01:00 committed by GitHub
parent 359b7cc4c0
commit f46ec293df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 400 additions and 258 deletions

View File

@ -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={<NetworkTrafficUsage />}
/>
<Route
path='backend-connections'
element={<BackendConnections />}
/>
</Routes>
</PageContent>
</div>

View File

@ -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 (
<ConditionallyRender
condition={isOss()}
show={<Alert severity='warning'>Not enabled.</Alert>}
elseShow={
<>
<StyledBox>
<TopRow>
<Box>
1 connection = 7200 backend SDK requests per day
</Box>
<PeriodSelector
selectedPeriod={chartDataSelection}
setPeriod={setChartDataSelection}
/>
</TopRow>
<Bar
data={chartData}
plugins={[customHighlightPlugin()]}
options={options}
aria-label={getChartLabel(chartDataSelection)}
/>
</StyledBox>
</>
}
/>
);
};
// 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;

View File

@ -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<ChartDataSelection>({
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,

View File

@ -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',
}));

View File

@ -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,
},
});

View File

@ -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);
});
});

View File

@ -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),
);
}
}

View File

@ -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<ChartDataSelection>({
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 };
};

View File

@ -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;
};