mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: traffic visibility UI and store (#6659)
Provides store method for retrieving traffic usage data based on period parameter, and UI + ui hook with the new chart for displaying traffic usage data spread out over selectable month. ![Skjermbilde 2024-03-21 kl 12 40 38](https://github.com/Unleash/unleash/assets/707867/539c6c98-b6f6-488a-97fb-baf4fccec687) In this PR we copied and adapted a plugin written by DX for highlighting a column in the chart: ![image](https://github.com/Unleash/unleash/assets/707867/70532b22-44ed-44c0-a9b4-75f65ed6a63d) There are some minor improvements planned which will come in a separate PR, reversing the order in legend and tooltip so the colors go from light to dark, and adding a month -sum below the legend ## Discussion points - Should any of this be extracted as a separate reusable component? --------- Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
parent
4b89c8a74a
commit
e0994b088a
@ -7,6 +7,9 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
|||||||
|
|
||||||
const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
|
const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
|
||||||
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
|
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
|
||||||
|
const NetworkTrafficUsage = lazy(
|
||||||
|
() => import('./NetworkTrafficUsage/NetworkTrafficUsage'),
|
||||||
|
);
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
@ -17,6 +20,10 @@ const tabs = [
|
|||||||
label: 'Traffic',
|
label: 'Traffic',
|
||||||
path: '/admin/network/traffic',
|
path: '/admin/network/traffic',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Data Usage',
|
||||||
|
path: '/admin/network/data-usage',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Network = () => {
|
export const Network = () => {
|
||||||
@ -52,6 +59,10 @@ export const Network = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path='traffic' element={<NetworkTraffic />} />
|
<Route path='traffic' element={<NetworkTraffic />} />
|
||||||
<Route path='*' element={<NetworkOverview />} />
|
<Route path='*' element={<NetworkOverview />} />
|
||||||
|
<Route
|
||||||
|
path='data-usage'
|
||||||
|
element={<NetworkTrafficUsage />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,392 @@
|
|||||||
|
import { useMemo, type VFC, useState, useEffect } 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 Alert from '@mui/material/Alert';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
type ChartOptions,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarElement,
|
||||||
|
type ChartDataset,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
type Chart,
|
||||||
|
type Tick,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
import { Bar } from 'react-chartjs-2';
|
||||||
|
import {
|
||||||
|
type IInstanceTrafficMetricsResponse,
|
||||||
|
useInstanceTrafficMetrics,
|
||||||
|
} from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||||
|
import type { Theme } from '@mui/material/styles/createTheme';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
|
type ChartDatasetType = ChartDataset<'bar'>;
|
||||||
|
|
||||||
|
type SelectablePeriod = {
|
||||||
|
key: string;
|
||||||
|
dayCount: number;
|
||||||
|
label: string;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EndpointInfo = {
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledHeader = styled('h3')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
margin: 0,
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const padMonth = (month: number): string =>
|
||||||
|
month < 10 ? `0${month}` : `${month}`;
|
||||||
|
|
||||||
|
const toSelectablePeriod = (date: Date, label?: string): SelectablePeriod => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
const period = `${year}-${padMonth(month + 1)}`;
|
||||||
|
const dayCount = new Date(year, month + 1, 0).getDate();
|
||||||
|
return {
|
||||||
|
key: period,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
dayCount,
|
||||||
|
label:
|
||||||
|
label ||
|
||||||
|
date.toLocaleString('en-US', { month: 'long', year: 'numeric' }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectablePeriods = (): SelectablePeriod[] => {
|
||||||
|
const current = new Date(Date.now());
|
||||||
|
const selectablePeriods = [toSelectablePeriod(current, 'Current month')];
|
||||||
|
for (
|
||||||
|
let subtractMonthCount = 1;
|
||||||
|
subtractMonthCount < 13;
|
||||||
|
subtractMonthCount++
|
||||||
|
) {
|
||||||
|
// JavaScript wraps around the year, so we don't need to handle that.
|
||||||
|
const date = new Date(
|
||||||
|
current.getFullYear(),
|
||||||
|
current.getMonth() - subtractMonthCount,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
selectablePeriods.push(toSelectablePeriod(date));
|
||||||
|
}
|
||||||
|
return selectablePeriods;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPeriodsRecord = (
|
||||||
|
periods: SelectablePeriod[],
|
||||||
|
): Record<string, SelectablePeriod> => {
|
||||||
|
return periods.reduce(
|
||||||
|
(acc, period) => {
|
||||||
|
acc[period.key] = period;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, SelectablePeriod>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDayLabels = (dayCount: number): number[] => {
|
||||||
|
return [...Array(dayCount).keys()].map((i) => i + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toChartData = (
|
||||||
|
days: number[],
|
||||||
|
traffic: IInstanceTrafficMetricsResponse,
|
||||||
|
endpointsInfo: Record<string, EndpointInfo>,
|
||||||
|
): ChartDatasetType[] => {
|
||||||
|
if (!traffic || !traffic.usage || !traffic.usage.apiData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = traffic.usage.apiData
|
||||||
|
.filter((item) => !!endpointsInfo[item.apiPath])
|
||||||
|
.sort(
|
||||||
|
(item1: any, item2: any) =>
|
||||||
|
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<string, number>,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const dayKey in item.days) {
|
||||||
|
const day = item.days[dayKey];
|
||||||
|
const dayNum = new Date(Date.parse(day.day)).getDate();
|
||||||
|
daysRec[`d${dayNum}`] = day.trafficTypes[0].count;
|
||||||
|
}
|
||||||
|
const epInfo = endpointsInfo[item.apiPath];
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: epInfo.label,
|
||||||
|
data: Object.values(daysRec),
|
||||||
|
backgroundColor: epInfo.color,
|
||||||
|
hoverBackgroundColor: epInfo.color,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const customHighlightPlugin = {
|
||||||
|
id: 'customLine',
|
||||||
|
beforeDraw: (chart: Chart) => {
|
||||||
|
const width = 36;
|
||||||
|
if (chart.tooltip?.opacity && chart.tooltip.x) {
|
||||||
|
const x = chart.tooltip.caretX;
|
||||||
|
const yAxis = chart.scales.y;
|
||||||
|
const ctx = chart.ctx;
|
||||||
|
ctx.save();
|
||||||
|
const gradient = ctx.createLinearGradient(
|
||||||
|
x,
|
||||||
|
yAxis.top,
|
||||||
|
x,
|
||||||
|
yAxis.bottom + 34,
|
||||||
|
);
|
||||||
|
gradient.addColorStop(0, 'rgba(129, 122, 254, 0)');
|
||||||
|
gradient.addColorStop(1, 'rgba(129, 122, 254, 0.12)');
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.roundRect(
|
||||||
|
x - width / 2,
|
||||||
|
yAxis.top,
|
||||||
|
width,
|
||||||
|
yAxis.bottom - yAxis.top + 34,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBarChartOptions = (
|
||||||
|
theme: Theme,
|
||||||
|
tooltipTitleCallback: (tooltipItems: any) => string,
|
||||||
|
): ChartOptions<'bar'> => ({
|
||||||
|
plugins: {
|
||||||
|
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: (
|
||||||
|
tickValue: string | number,
|
||||||
|
index: number,
|
||||||
|
ticks: Tick[],
|
||||||
|
) => {
|
||||||
|
if (typeof tickValue === 'string') {
|
||||||
|
return tickValue;
|
||||||
|
}
|
||||||
|
const value = Number.parseInt(tickValue.toString());
|
||||||
|
if (value > 999999) {
|
||||||
|
return `${value / 1000000}M`;
|
||||||
|
}
|
||||||
|
return value > 999 ? `${value / 1000}k` : value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawBorder: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
bar: {
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NetworkTrafficUsage: VFC = () => {
|
||||||
|
usePageTitle('Network - Data Usage');
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const endpointsInfo: Record<string, EndpointInfo> = {
|
||||||
|
'/api/admin': {
|
||||||
|
label: 'Admin',
|
||||||
|
color: '#6D66D9',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
'/api/frontend': {
|
||||||
|
label: 'Frontend',
|
||||||
|
color: '#A39EFF',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
'/api/client': {
|
||||||
|
label: 'Server',
|
||||||
|
color: '#D8D6FF',
|
||||||
|
order: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectablePeriods = getSelectablePeriods();
|
||||||
|
const record = toPeriodsRecord(selectablePeriods);
|
||||||
|
const [period, setPeriod] = useState<string>(selectablePeriods[0].key);
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return createBarChartOptions(theme, (tooltipItems: any) => {
|
||||||
|
const periodItem = record[period];
|
||||||
|
const tooltipDate = new Date(
|
||||||
|
periodItem.year,
|
||||||
|
periodItem.month,
|
||||||
|
Number.parseInt(tooltipItems[0].label),
|
||||||
|
);
|
||||||
|
return tooltipDate.toLocaleDateString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [theme, period]);
|
||||||
|
|
||||||
|
const traffic = useInstanceTrafficMetrics(period);
|
||||||
|
|
||||||
|
const [labels, setLabels] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const [datasets, setDatasets] = useState<ChartDatasetType[]>([]);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels,
|
||||||
|
datasets,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isOss } = useUiConfig();
|
||||||
|
const flagEnabled = useUiFlag('collectTrafficDataUsage');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDatasets(toChartData(labels, traffic, endpointsInfo));
|
||||||
|
}, [labels, traffic]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (record && period) {
|
||||||
|
const periodData = record[period];
|
||||||
|
setLabels(getDayLabels(periodData.dayCount));
|
||||||
|
}
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isOss() || !flagEnabled}
|
||||||
|
show={<Alert severity='warning'>No data available.</Alert>}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<Grid container component='header' spacing={2}>
|
||||||
|
<Grid item xs={12} md={10}>
|
||||||
|
<StyledHeader>
|
||||||
|
Number of requests to Unleash
|
||||||
|
</StyledHeader>
|
||||||
|
</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>
|
||||||
|
<Box sx={{ display: 'grid', gap: 4 }}>
|
||||||
|
<div>
|
||||||
|
<Bar
|
||||||
|
data={data}
|
||||||
|
plugins={[customHighlightPlugin]}
|
||||||
|
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'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register dependencies that we need to draw the chart.
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use a default export to lazy-load the charting library.
|
||||||
|
export default NetworkTrafficUsage;
|
@ -0,0 +1,40 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import type { TrafficUsageDataSegmentedSchema } from 'openapi';
|
||||||
|
|
||||||
|
export interface IInstanceTrafficMetricsResponse {
|
||||||
|
usage: TrafficUsageDataSegmentedSchema;
|
||||||
|
|
||||||
|
refetch: () => void;
|
||||||
|
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInstanceTrafficMetrics = (
|
||||||
|
period: string,
|
||||||
|
): IInstanceTrafficMetricsResponse => {
|
||||||
|
const { data, error, mutate } = useSWR(
|
||||||
|
formatApiPath(`api/admin/metrics/traffic/${period}`),
|
||||||
|
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'))
|
||||||
|
.then((res) => res.json());
|
||||||
|
};
|
@ -80,6 +80,7 @@ export type UiFlags = {
|
|||||||
sdkReporting?: boolean;
|
sdkReporting?: boolean;
|
||||||
outdatedSdksBanner?: boolean;
|
outdatedSdksBanner?: boolean;
|
||||||
projectOverviewRefactor?: string;
|
projectOverviewRefactor?: string;
|
||||||
|
collectTrafficDataUsage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -1076,6 +1076,10 @@ export * from './toggleMaintenance403';
|
|||||||
export * from './toggleMaintenanceSchema';
|
export * from './toggleMaintenanceSchema';
|
||||||
export * from './tokenStringListSchema';
|
export * from './tokenStringListSchema';
|
||||||
export * from './tokenUserSchema';
|
export * from './tokenUserSchema';
|
||||||
|
export * from './trafficUsageApiDataSchema';
|
||||||
|
export * from './trafficUsageApiDataSchemaDaysItem';
|
||||||
|
export * from './trafficUsageApiDataSchemaDaysItemTrafficTypesItem';
|
||||||
|
export * from './trafficUsageDataSegmentedSchema';
|
||||||
export * from './uiConfigSchema';
|
export * from './uiConfigSchema';
|
||||||
export * from './uiConfigSchemaAuthenticationType';
|
export * from './uiConfigSchemaAuthenticationType';
|
||||||
export * from './uiConfigSchemaFlags';
|
export * from './uiConfigSchemaFlags';
|
||||||
|
16
frontend/src/openapi/models/trafficUsageApiDataSchema.ts
Normal file
16
frontend/src/openapi/models/trafficUsageApiDataSchema.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Generated by Orval
|
||||||
|
* Do not edit manually.
|
||||||
|
* See `gen:api` script in package.json
|
||||||
|
*/
|
||||||
|
import type { TrafficUsageApiDataSchemaDaysItem } from './trafficUsageApiDataSchemaDaysItem';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the recorded data usage for each API path, segmented by day and type of traffic
|
||||||
|
*/
|
||||||
|
export interface TrafficUsageApiDataSchema {
|
||||||
|
/** The path of the API that the recorded data usage is for */
|
||||||
|
apiPath: string;
|
||||||
|
/** An array containing each day in the selected period that has data usage recorded */
|
||||||
|
days: TrafficUsageApiDataSchemaDaysItem[];
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Generated by Orval
|
||||||
|
* Do not edit manually.
|
||||||
|
* See `gen:api` script in package.json
|
||||||
|
*/
|
||||||
|
import type { TrafficUsageApiDataSchemaDaysItemTrafficTypesItem } from './trafficUsageApiDataSchemaDaysItemTrafficTypesItem';
|
||||||
|
|
||||||
|
export type TrafficUsageApiDataSchemaDaysItem = {
|
||||||
|
/** The day of the period for which the usage is recorded */
|
||||||
|
day: string;
|
||||||
|
/** Contains the recorded data usage for each type of traffic group */
|
||||||
|
trafficTypes: TrafficUsageApiDataSchemaDaysItemTrafficTypesItem[];
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Generated by Orval
|
||||||
|
* Do not edit manually.
|
||||||
|
* See `gen:api` script in package.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type TrafficUsageApiDataSchemaDaysItemTrafficTypesItem = {
|
||||||
|
/** The number of requests */
|
||||||
|
count: number;
|
||||||
|
/** The traffic group */
|
||||||
|
group: string;
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Generated by Orval
|
||||||
|
* Do not edit manually.
|
||||||
|
* See `gen:api` script in package.json
|
||||||
|
*/
|
||||||
|
import type { TrafficUsageApiDataSchema } from './trafficUsageApiDataSchema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the recorded data usage for each API path, segmented by day and type of traffic
|
||||||
|
*/
|
||||||
|
export interface TrafficUsageDataSegmentedSchema {
|
||||||
|
/** Contains the recorded daily data usage for each API path */
|
||||||
|
apiData: TrafficUsageApiDataSchema[];
|
||||||
|
/** The year-month period for which the data usage is counted */
|
||||||
|
period: string;
|
||||||
|
}
|
@ -26,4 +26,7 @@ export class FakeTrafficDataUsageStore implements ITrafficDataUsageStore {
|
|||||||
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void> {
|
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
getTrafficDataUsageForPeriod(period: string): Promise<IStatTrafficUsage[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ export type IStatTrafficUsage = {
|
|||||||
day: Date;
|
day: Date;
|
||||||
trafficGroup: string;
|
trafficGroup: string;
|
||||||
statusCodeSeries: number;
|
statusCodeSeries: number;
|
||||||
count: number | string;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IStatTrafficUsageKey {
|
export interface IStatTrafficUsageKey {
|
||||||
@ -16,4 +16,5 @@ export interface IStatTrafficUsageKey {
|
|||||||
export interface ITrafficDataUsageStore
|
export interface ITrafficDataUsageStore
|
||||||
extends Store<IStatTrafficUsage, IStatTrafficUsageKey> {
|
extends Store<IStatTrafficUsage, IStatTrafficUsageKey> {
|
||||||
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void>;
|
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void>;
|
||||||
|
getTrafficDataUsageForPeriod(period: string): Promise<IStatTrafficUsage[]>;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ test('upsert stores new entries', async () => {
|
|||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default',
|
trafficGroup: 'default',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '1',
|
count: 1,
|
||||||
};
|
};
|
||||||
await trafficDataUsageStore.upsert(data);
|
await trafficDataUsageStore.upsert(data);
|
||||||
const data2 = await trafficDataUsageStore.get({
|
const data2 = await trafficDataUsageStore.get({
|
||||||
@ -34,7 +34,7 @@ test('upsert stores new entries', async () => {
|
|||||||
statusCodeSeries: data.statusCodeSeries,
|
statusCodeSeries: data.statusCodeSeries,
|
||||||
});
|
});
|
||||||
expect(data2).toBeDefined();
|
expect(data2).toBeDefined();
|
||||||
expect(data2.count).toBe('1');
|
expect(data2.count).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('upsert upserts', async () => {
|
test('upsert upserts', async () => {
|
||||||
@ -42,13 +42,13 @@ test('upsert upserts', async () => {
|
|||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default2',
|
trafficGroup: 'default2',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '1',
|
count: 1,
|
||||||
};
|
};
|
||||||
const dataSecondTime = {
|
const dataSecondTime = {
|
||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default2',
|
trafficGroup: 'default2',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '3',
|
count: 3,
|
||||||
};
|
};
|
||||||
await trafficDataUsageStore.upsert(data);
|
await trafficDataUsageStore.upsert(data);
|
||||||
await trafficDataUsageStore.upsert(dataSecondTime);
|
await trafficDataUsageStore.upsert(dataSecondTime);
|
||||||
@ -58,7 +58,7 @@ test('upsert upserts', async () => {
|
|||||||
statusCodeSeries: data.statusCodeSeries,
|
statusCodeSeries: data.statusCodeSeries,
|
||||||
});
|
});
|
||||||
expect(data2).toBeDefined();
|
expect(data2).toBeDefined();
|
||||||
expect(data2.count).toBe('4');
|
expect(data2.count).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getAll returns all', async () => {
|
test('getAll returns all', async () => {
|
||||||
@ -67,13 +67,13 @@ test('getAll returns all', async () => {
|
|||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default3',
|
trafficGroup: 'default3',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '1',
|
count: 1,
|
||||||
};
|
};
|
||||||
const data2 = {
|
const data2 = {
|
||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default4',
|
trafficGroup: 'default4',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '3',
|
count: 3,
|
||||||
};
|
};
|
||||||
await trafficDataUsageStore.upsert(data1);
|
await trafficDataUsageStore.upsert(data1);
|
||||||
await trafficDataUsageStore.upsert(data2);
|
await trafficDataUsageStore.upsert(data2);
|
||||||
@ -88,13 +88,13 @@ test('delete deletes the specified item', async () => {
|
|||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default3',
|
trafficGroup: 'default3',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '1',
|
count: 1,
|
||||||
};
|
};
|
||||||
const data2 = {
|
const data2 = {
|
||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default4',
|
trafficGroup: 'default4',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '3',
|
count: 3,
|
||||||
};
|
};
|
||||||
await trafficDataUsageStore.upsert(data1);
|
await trafficDataUsageStore.upsert(data1);
|
||||||
await trafficDataUsageStore.upsert(data2);
|
await trafficDataUsageStore.upsert(data2);
|
||||||
@ -115,19 +115,19 @@ test('can query for specific items', async () => {
|
|||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default3',
|
trafficGroup: 'default3',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '1',
|
count: 1,
|
||||||
};
|
};
|
||||||
const data2 = {
|
const data2 = {
|
||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default4',
|
trafficGroup: 'default4',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '3',
|
count: 3,
|
||||||
};
|
};
|
||||||
const data3 = {
|
const data3 = {
|
||||||
day: new Date(),
|
day: new Date(),
|
||||||
trafficGroup: 'default5',
|
trafficGroup: 'default5',
|
||||||
statusCodeSeries: 200,
|
statusCodeSeries: 200,
|
||||||
count: '2',
|
count: 2,
|
||||||
};
|
};
|
||||||
await trafficDataUsageStore.upsert(data1);
|
await trafficDataUsageStore.upsert(data1);
|
||||||
await trafficDataUsageStore.upsert(data2);
|
await trafficDataUsageStore.upsert(data2);
|
||||||
@ -152,3 +152,46 @@ test('can query for specific items', async () => {
|
|||||||
expect(results_status_code).toBeDefined();
|
expect(results_status_code).toBeDefined();
|
||||||
expect(results_status_code.length).toBe(3);
|
expect(results_status_code.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can query for data from specific periods', async () => {
|
||||||
|
await trafficDataUsageStore.deleteAll();
|
||||||
|
const data1 = {
|
||||||
|
day: new Date(2024, 2, 12),
|
||||||
|
trafficGroup: 'default-period-query',
|
||||||
|
statusCodeSeries: 200,
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
const data2 = {
|
||||||
|
day: new Date(2024, 2, 13),
|
||||||
|
trafficGroup: 'default-period-query',
|
||||||
|
statusCodeSeries: 200,
|
||||||
|
count: 3,
|
||||||
|
};
|
||||||
|
const data3 = {
|
||||||
|
day: new Date(2024, 1, 12),
|
||||||
|
trafficGroup: 'default-period-query',
|
||||||
|
statusCodeSeries: 200,
|
||||||
|
count: 2,
|
||||||
|
};
|
||||||
|
const data4 = {
|
||||||
|
day: new Date(2023, 9, 6),
|
||||||
|
trafficGroup: 'default-period-query',
|
||||||
|
statusCodeSeries: 200,
|
||||||
|
count: 12,
|
||||||
|
};
|
||||||
|
await trafficDataUsageStore.upsert(data1);
|
||||||
|
await trafficDataUsageStore.upsert(data2);
|
||||||
|
await trafficDataUsageStore.upsert(data3);
|
||||||
|
await trafficDataUsageStore.upsert(data4);
|
||||||
|
|
||||||
|
const traffic_period_usage =
|
||||||
|
await trafficDataUsageStore.getTrafficDataUsageForPeriod('2024-03');
|
||||||
|
expect(traffic_period_usage).toBeDefined();
|
||||||
|
expect(traffic_period_usage.length).toBe(2);
|
||||||
|
|
||||||
|
const traffic_period_usage_older =
|
||||||
|
await trafficDataUsageStore.getTrafficDataUsageForPeriod('2023-10');
|
||||||
|
expect(traffic_period_usage_older).toBeDefined();
|
||||||
|
expect(traffic_period_usage_older.length).toBe(1);
|
||||||
|
expect(traffic_period_usage_older[0].count).toBe(12);
|
||||||
|
});
|
||||||
|
@ -23,7 +23,7 @@ const mapRow = (row: any): IStatTrafficUsage => {
|
|||||||
day: row.day,
|
day: row.day,
|
||||||
trafficGroup: row.traffic_group,
|
trafficGroup: row.traffic_group,
|
||||||
statusCodeSeries: row.status_code_series,
|
statusCodeSeries: row.status_code_series,
|
||||||
count: row.count,
|
count: Number.parseInt(row.count),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -87,4 +87,14 @@ export class TrafficDataUsageStore implements ITrafficDataUsageStore {
|
|||||||
count: this.db.raw('stat_traffic_usage.count + EXCLUDED.count'),
|
count: this.db.raw('stat_traffic_usage.count + EXCLUDED.count'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTrafficDataUsageForPeriod(
|
||||||
|
period: string,
|
||||||
|
): Promise<IStatTrafficUsage[]> {
|
||||||
|
const rows = await this.db<IStatTrafficUsage>(TABLE).whereRaw(
|
||||||
|
`to_char(day, 'YYYY-MM') = ?`,
|
||||||
|
[period],
|
||||||
|
);
|
||||||
|
return rows.map(mapRow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user