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 NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
|
||||
const NetworkTrafficUsage = lazy(
|
||||
() => import('./NetworkTrafficUsage/NetworkTrafficUsage'),
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
@ -17,6 +20,10 @@ const tabs = [
|
||||
label: 'Traffic',
|
||||
path: '/admin/network/traffic',
|
||||
},
|
||||
{
|
||||
label: 'Data Usage',
|
||||
path: '/admin/network/data-usage',
|
||||
},
|
||||
];
|
||||
|
||||
export const Network = () => {
|
||||
@ -52,6 +59,10 @@ export const Network = () => {
|
||||
<Routes>
|
||||
<Route path='traffic' element={<NetworkTraffic />} />
|
||||
<Route path='*' element={<NetworkOverview />} />
|
||||
<Route
|
||||
path='data-usage'
|
||||
element={<NetworkTrafficUsage />}
|
||||
/>
|
||||
</Routes>
|
||||
</PageContent>
|
||||
</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;
|
||||
outdatedSdksBanner?: boolean;
|
||||
projectOverviewRefactor?: string;
|
||||
collectTrafficDataUsage?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -1076,6 +1076,10 @@ export * from './toggleMaintenance403';
|
||||
export * from './toggleMaintenanceSchema';
|
||||
export * from './tokenStringListSchema';
|
||||
export * from './tokenUserSchema';
|
||||
export * from './trafficUsageApiDataSchema';
|
||||
export * from './trafficUsageApiDataSchemaDaysItem';
|
||||
export * from './trafficUsageApiDataSchemaDaysItemTrafficTypesItem';
|
||||
export * from './trafficUsageDataSegmentedSchema';
|
||||
export * from './uiConfigSchema';
|
||||
export * from './uiConfigSchemaAuthenticationType';
|
||||
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> {
|
||||
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;
|
||||
trafficGroup: string;
|
||||
statusCodeSeries: number;
|
||||
count: number | string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export interface IStatTrafficUsageKey {
|
||||
@ -16,4 +16,5 @@ export interface IStatTrafficUsageKey {
|
||||
export interface ITrafficDataUsageStore
|
||||
extends Store<IStatTrafficUsage, IStatTrafficUsageKey> {
|
||||
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void>;
|
||||
getTrafficDataUsageForPeriod(period: string): Promise<IStatTrafficUsage[]>;
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ test('upsert stores new entries', async () => {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default',
|
||||
statusCodeSeries: 200,
|
||||
count: '1',
|
||||
count: 1,
|
||||
};
|
||||
await trafficDataUsageStore.upsert(data);
|
||||
const data2 = await trafficDataUsageStore.get({
|
||||
@ -34,7 +34,7 @@ test('upsert stores new entries', async () => {
|
||||
statusCodeSeries: data.statusCodeSeries,
|
||||
});
|
||||
expect(data2).toBeDefined();
|
||||
expect(data2.count).toBe('1');
|
||||
expect(data2.count).toBe(1);
|
||||
});
|
||||
|
||||
test('upsert upserts', async () => {
|
||||
@ -42,13 +42,13 @@ test('upsert upserts', async () => {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default2',
|
||||
statusCodeSeries: 200,
|
||||
count: '1',
|
||||
count: 1,
|
||||
};
|
||||
const dataSecondTime = {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default2',
|
||||
statusCodeSeries: 200,
|
||||
count: '3',
|
||||
count: 3,
|
||||
};
|
||||
await trafficDataUsageStore.upsert(data);
|
||||
await trafficDataUsageStore.upsert(dataSecondTime);
|
||||
@ -58,7 +58,7 @@ test('upsert upserts', async () => {
|
||||
statusCodeSeries: data.statusCodeSeries,
|
||||
});
|
||||
expect(data2).toBeDefined();
|
||||
expect(data2.count).toBe('4');
|
||||
expect(data2.count).toBe(4);
|
||||
});
|
||||
|
||||
test('getAll returns all', async () => {
|
||||
@ -67,13 +67,13 @@ test('getAll returns all', async () => {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default3',
|
||||
statusCodeSeries: 200,
|
||||
count: '1',
|
||||
count: 1,
|
||||
};
|
||||
const data2 = {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default4',
|
||||
statusCodeSeries: 200,
|
||||
count: '3',
|
||||
count: 3,
|
||||
};
|
||||
await trafficDataUsageStore.upsert(data1);
|
||||
await trafficDataUsageStore.upsert(data2);
|
||||
@ -88,13 +88,13 @@ test('delete deletes the specified item', async () => {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default3',
|
||||
statusCodeSeries: 200,
|
||||
count: '1',
|
||||
count: 1,
|
||||
};
|
||||
const data2 = {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default4',
|
||||
statusCodeSeries: 200,
|
||||
count: '3',
|
||||
count: 3,
|
||||
};
|
||||
await trafficDataUsageStore.upsert(data1);
|
||||
await trafficDataUsageStore.upsert(data2);
|
||||
@ -115,19 +115,19 @@ test('can query for specific items', async () => {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default3',
|
||||
statusCodeSeries: 200,
|
||||
count: '1',
|
||||
count: 1,
|
||||
};
|
||||
const data2 = {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default4',
|
||||
statusCodeSeries: 200,
|
||||
count: '3',
|
||||
count: 3,
|
||||
};
|
||||
const data3 = {
|
||||
day: new Date(),
|
||||
trafficGroup: 'default5',
|
||||
statusCodeSeries: 200,
|
||||
count: '2',
|
||||
count: 2,
|
||||
};
|
||||
await trafficDataUsageStore.upsert(data1);
|
||||
await trafficDataUsageStore.upsert(data2);
|
||||
@ -152,3 +152,46 @@ test('can query for specific items', async () => {
|
||||
expect(results_status_code).toBeDefined();
|
||||
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,
|
||||
trafficGroup: row.traffic_group,
|
||||
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'),
|
||||
});
|
||||
}
|
||||
|
||||
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