mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: first draft of chart for instance traffic in frontend (#2670)
## What We've already added the backend for this. This is the initial work for drawing a chart for instance traffic in the frontend. It requires the environment variable `PROMETHEUS_API` set to a valid prometheus-query-language (promql) supported backend, such as Prometheus itself or Victoria Metrics. Besides, at the moment we're hiding this functionality behind the flag `UNLEASH_EXPERIMENTAL_NETWORK_VIEW` which has to be set to true Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai> Co-authored-by: Gastón Fournier <gaston@getunleash.ai>
This commit is contained in:
parent
8e5ca352c3
commit
23094b016e
@ -87,6 +87,16 @@ function AdminMenu() {
|
|||||||
</CenteredNavLink>
|
</CenteredNavLink>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{flags.networkView && (
|
||||||
|
<Tab
|
||||||
|
value="/admin/traffic"
|
||||||
|
label={
|
||||||
|
<CenteredNavLink to="/admin/traffic">
|
||||||
|
Traffic
|
||||||
|
</CenteredNavLink>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isBilling && (
|
{isBilling && (
|
||||||
<Tab
|
<Tab
|
||||||
value="/admin/billing"
|
value="/admin/billing"
|
||||||
|
11
frontend/src/component/admin/traffic/Traffic.tsx
Normal file
11
frontend/src/component/admin/traffic/Traffic.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import InstanceMetricsChart from './TrafficMetricsChart';
|
||||||
|
import AdminMenu from '../menu/AdminMenu';
|
||||||
|
|
||||||
|
export const Traffic = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AdminMenu />
|
||||||
|
<InstanceMetricsChart />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
212
frontend/src/component/admin/traffic/TrafficMetricsChart.tsx
Normal file
212
frontend/src/component/admin/traffic/TrafficMetricsChart.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import {
|
||||||
|
InstanceMetrics,
|
||||||
|
useInstanceMetrics,
|
||||||
|
} from '../../../hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
|
||||||
|
import { useMemo, VFC } from 'react';
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
import {
|
||||||
|
CategoryScale,
|
||||||
|
Chart as ChartJS,
|
||||||
|
ChartData,
|
||||||
|
ChartDataset,
|
||||||
|
ChartOptions,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
TimeScale,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from 'chart.js';
|
||||||
|
import {
|
||||||
|
ILocationSettings,
|
||||||
|
useLocationSettings,
|
||||||
|
} from '../../../hooks/useLocationSettings';
|
||||||
|
import theme from '../../../themes/theme';
|
||||||
|
import { formatDateHM } from '../../../utils/formatDate';
|
||||||
|
import { RequestsPerSecondSchema } from 'openapi';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
import { PaletteColor } from '@mui/material';
|
||||||
|
import { PageContent } from '../../common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from '../../common/PageHeader/PageHeader';
|
||||||
|
import { Box } from '@mui/system';
|
||||||
|
import { current } from 'immer';
|
||||||
|
import { CyclicIterator } from 'utils/cyclicIterator';
|
||||||
|
interface IPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartDatasetType = ChartDataset<'line', IPoint[]>;
|
||||||
|
type ChartDataType = ChartData<'line', IPoint[], string>;
|
||||||
|
|
||||||
|
const createChartPoints = (
|
||||||
|
values: Array<Array<number | string>>,
|
||||||
|
y: (m: string) => number
|
||||||
|
): IPoint[] => {
|
||||||
|
return values.map(row => ({
|
||||||
|
x: row[0] as number,
|
||||||
|
y: y(row[1] as string),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const createInstanceChartOptions = (
|
||||||
|
metrics: InstanceMetrics,
|
||||||
|
locationSettings: ILocationSettings
|
||||||
|
): ChartOptions<'line'> => {
|
||||||
|
return {
|
||||||
|
locale: locationSettings.locale,
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
bodyColor: theme.palette.text.primary,
|
||||||
|
titleColor: theme.palette.grey[700],
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 10,
|
||||||
|
boxPadding: 5,
|
||||||
|
usePointStyle: true,
|
||||||
|
callbacks: {
|
||||||
|
title: items =>
|
||||||
|
formatDateHM(
|
||||||
|
1000 * items[0].parsed.x,
|
||||||
|
locationSettings.locale
|
||||||
|
),
|
||||||
|
},
|
||||||
|
itemSort: (a, b) => b.parsed.y - a.parsed.y,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
align: 'end',
|
||||||
|
labels: {
|
||||||
|
boxWidth: 10,
|
||||||
|
boxHeight: 10,
|
||||||
|
usePointStyle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: 'Requests per second in the last 6 hours',
|
||||||
|
position: 'top',
|
||||||
|
align: 'start',
|
||||||
|
display: true,
|
||||||
|
font: {
|
||||||
|
size: 16,
|
||||||
|
weight: '400',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Requests per second',
|
||||||
|
},
|
||||||
|
// min: 0,
|
||||||
|
suggestedMin: 0,
|
||||||
|
ticks: { precision: 0 },
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: { unit: 'minute' },
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: {
|
||||||
|
callback: (_, i, data) =>
|
||||||
|
formatDateHM(data[i].value, locationSettings.locale),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const toChartData = (
|
||||||
|
rps: RequestsPerSecondSchema,
|
||||||
|
color: PaletteColor,
|
||||||
|
label: (name: string) => string
|
||||||
|
): ChartDatasetType[] => {
|
||||||
|
if (rps.data?.result) {
|
||||||
|
return rps.data.result.map(dataset => ({
|
||||||
|
label: label(dataset.metric?.appName || 'unknown'),
|
||||||
|
borderColor: color.main,
|
||||||
|
backgroundColor: color.main,
|
||||||
|
data: createChartPoints(dataset.values || [], y => parseFloat(y)),
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 4,
|
||||||
|
pointStyle: 'circle',
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
borderDash: [8, 4],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInstanceChartData = (metrics?: InstanceMetrics): ChartDataType => {
|
||||||
|
if (metrics) {
|
||||||
|
const colors = new CyclicIterator<PaletteColor>([
|
||||||
|
theme.palette.success,
|
||||||
|
theme.palette.error,
|
||||||
|
theme.palette.primary,
|
||||||
|
]);
|
||||||
|
let datasets: ChartDatasetType[] = [];
|
||||||
|
for (let key in metrics) {
|
||||||
|
datasets = datasets.concat(
|
||||||
|
toChartData(
|
||||||
|
metrics[key],
|
||||||
|
colors.next(),
|
||||||
|
metricName => `${metricName}: ${key}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { datasets };
|
||||||
|
}
|
||||||
|
return { datasets: [] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InstanceMetricsChart: VFC = () => {
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
const { metrics } = useInstanceMetrics();
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return createInstanceChartOptions(metrics, locationSettings);
|
||||||
|
}, [metrics, locationSettings]);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return createInstanceChartData(metrics);
|
||||||
|
}, [metrics, locationSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent header={<PageHeader title="Requests per second" />}>
|
||||||
|
<Box sx={{ display: 'grid', gap: 4 }}>
|
||||||
|
<div style={{ height: 400 }}>
|
||||||
|
<Line
|
||||||
|
data={data}
|
||||||
|
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>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// Register dependencies that we need to draw the chart.
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Tooltip,
|
||||||
|
Title
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use a default export to lazy-load the charting library.
|
||||||
|
export default InstanceMetricsChart;
|
@ -465,6 +465,17 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Instance stats",
|
"title": "Instance stats",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"flag": "networkView",
|
||||||
|
"menu": {
|
||||||
|
"adminSettings": true,
|
||||||
|
},
|
||||||
|
"parent": "/admin",
|
||||||
|
"path": "/admin/traffic",
|
||||||
|
"title": "Traffic",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"flag": "embedProxyFrontend",
|
"flag": "embedProxyFrontend",
|
||||||
|
@ -60,6 +60,7 @@ import { CorsAdmin } from 'component/admin/cors';
|
|||||||
import { InviteLink } from 'component/admin/users/InviteLink/InviteLink';
|
import { InviteLink } from 'component/admin/users/InviteLink/InviteLink';
|
||||||
import { Profile } from 'component/user/Profile/Profile';
|
import { Profile } from 'component/user/Profile/Profile';
|
||||||
import { InstanceAdmin } from '../admin/instance-admin/InstanceAdmin';
|
import { InstanceAdmin } from '../admin/instance-admin/InstanceAdmin';
|
||||||
|
import { Traffic } from 'component/admin/traffic/Traffic';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -509,6 +510,15 @@ export const routes: IRoute[] = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/traffic',
|
||||||
|
parent: '/admin',
|
||||||
|
title: 'Traffic',
|
||||||
|
component: Traffic,
|
||||||
|
type: 'protected',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
flag: 'networkView',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/cors',
|
path: '/admin/cors',
|
||||||
parent: '/admin',
|
parent: '/admin',
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { RequestsPerSecondSchema } from 'openapi';
|
||||||
|
|
||||||
|
export interface InstanceMetrics {
|
||||||
|
[key: string]: RequestsPerSecondSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInstanceMetricsResponse {
|
||||||
|
metrics: InstanceMetrics;
|
||||||
|
|
||||||
|
refetch: () => void;
|
||||||
|
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInstanceMetrics = (): IInstanceMetricsResponse => {
|
||||||
|
const { data, error, mutate } = useSWR(
|
||||||
|
formatApiPath(`api/admin/metrics/rps`),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
metrics: 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
frontend/src/openapi/models/RequestsPerSecondSchema.ts
Normal file
80
frontend/src/openapi/models/RequestsPerSecondSchema.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Unleash API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 4.19.0-beta.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exists, mapValues } from '../runtime';
|
||||||
|
import type { RequestsPerSecondSchemaData } from './RequestsPerSecondSchemaData';
|
||||||
|
import {
|
||||||
|
RequestsPerSecondSchemaDataFromJSON,
|
||||||
|
RequestsPerSecondSchemaDataFromJSONTyped,
|
||||||
|
RequestsPerSecondSchemaDataToJSON,
|
||||||
|
} from './RequestsPerSecondSchemaData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface RequestsPerSecondSchema
|
||||||
|
*/
|
||||||
|
export interface RequestsPerSecondSchema {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof RequestsPerSecondSchema
|
||||||
|
*/
|
||||||
|
status?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {RequestsPerSecondSchemaData}
|
||||||
|
* @memberof RequestsPerSecondSchema
|
||||||
|
*/
|
||||||
|
data?: RequestsPerSecondSchemaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the RequestsPerSecondSchema interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfRequestsPerSecondSchema(value: object): boolean {
|
||||||
|
let isInstance = true;
|
||||||
|
|
||||||
|
return isInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaFromJSON(json: any): RequestsPerSecondSchema {
|
||||||
|
return RequestsPerSecondSchemaFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchema {
|
||||||
|
if ((json === undefined) || (json === null)) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'status': !exists(json, 'status') ? undefined : json['status'],
|
||||||
|
'data': !exists(json, 'data') ? undefined : RequestsPerSecondSchemaDataFromJSON(json['data']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaToJSON(value?: RequestsPerSecondSchema | null): any {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'status': value.status,
|
||||||
|
'data': RequestsPerSecondSchemaDataToJSON(value.data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
80
frontend/src/openapi/models/RequestsPerSecondSchemaData.ts
Normal file
80
frontend/src/openapi/models/RequestsPerSecondSchemaData.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Unleash API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 4.19.0-beta.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exists, mapValues } from '../runtime';
|
||||||
|
import type { RequestsPerSecondSchemaDataResultInner } from './RequestsPerSecondSchemaDataResultInner';
|
||||||
|
import {
|
||||||
|
RequestsPerSecondSchemaDataResultInnerFromJSON,
|
||||||
|
RequestsPerSecondSchemaDataResultInnerFromJSONTyped,
|
||||||
|
RequestsPerSecondSchemaDataResultInnerToJSON,
|
||||||
|
} from './RequestsPerSecondSchemaDataResultInner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface RequestsPerSecondSchemaData
|
||||||
|
*/
|
||||||
|
export interface RequestsPerSecondSchemaData {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof RequestsPerSecondSchemaData
|
||||||
|
*/
|
||||||
|
resultType?: string;
|
||||||
|
/**
|
||||||
|
* An array of values per metric. Each one represents a line in the graph labeled by its metric name
|
||||||
|
* @type {Array<RequestsPerSecondSchemaDataResultInner>}
|
||||||
|
* @memberof RequestsPerSecondSchemaData
|
||||||
|
*/
|
||||||
|
result?: Array<RequestsPerSecondSchemaDataResultInner>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the RequestsPerSecondSchemaData interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfRequestsPerSecondSchemaData(value: object): boolean {
|
||||||
|
let isInstance = true;
|
||||||
|
|
||||||
|
return isInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataFromJSON(json: any): RequestsPerSecondSchemaData {
|
||||||
|
return RequestsPerSecondSchemaDataFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaData {
|
||||||
|
if ((json === undefined) || (json === null)) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'resultType': !exists(json, 'resultType') ? undefined : json['resultType'],
|
||||||
|
'result': !exists(json, 'result') ? undefined : ((json['result'] as Array<any>).map(RequestsPerSecondSchemaDataResultInnerFromJSON)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataToJSON(value?: RequestsPerSecondSchemaData | null): any {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'resultType': value.resultType,
|
||||||
|
'result': value.result === undefined ? undefined : ((value.result as Array<any>).map(RequestsPerSecondSchemaDataResultInnerToJSON)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Unleash API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 4.19.0-beta.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exists, mapValues } from '../runtime';
|
||||||
|
import type { RequestsPerSecondSchemaDataResultInnerMetric } from './RequestsPerSecondSchemaDataResultInnerMetric';
|
||||||
|
import {
|
||||||
|
RequestsPerSecondSchemaDataResultInnerMetricFromJSON,
|
||||||
|
RequestsPerSecondSchemaDataResultInnerMetricFromJSONTyped,
|
||||||
|
RequestsPerSecondSchemaDataResultInnerMetricToJSON,
|
||||||
|
} from './RequestsPerSecondSchemaDataResultInnerMetric';
|
||||||
|
import type { RequestsPerSecondSchemaDataResultInnerValuesInnerInner } from './RequestsPerSecondSchemaDataResultInnerValuesInnerInner';
|
||||||
|
import {
|
||||||
|
RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSON,
|
||||||
|
RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSONTyped,
|
||||||
|
RequestsPerSecondSchemaDataResultInnerValuesInnerInnerToJSON,
|
||||||
|
} from './RequestsPerSecondSchemaDataResultInnerValuesInnerInner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface RequestsPerSecondSchemaDataResultInner
|
||||||
|
*/
|
||||||
|
export interface RequestsPerSecondSchemaDataResultInner {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {RequestsPerSecondSchemaDataResultInnerMetric}
|
||||||
|
* @memberof RequestsPerSecondSchemaDataResultInner
|
||||||
|
*/
|
||||||
|
metric?: RequestsPerSecondSchemaDataResultInnerMetric;
|
||||||
|
/**
|
||||||
|
* An array of arrays. Each element of the array is an array of size 2 consisting of the 2 axis for the graph: in position zero the x axis represented as a number and position one the y axis represented as string
|
||||||
|
* @type {Array<Array<RequestsPerSecondSchemaDataResultInnerValuesInnerInner>>}
|
||||||
|
* @memberof RequestsPerSecondSchemaDataResultInner
|
||||||
|
*/
|
||||||
|
values?: Array<Array<RequestsPerSecondSchemaDataResultInnerValuesInnerInner>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the RequestsPerSecondSchemaDataResultInner interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfRequestsPerSecondSchemaDataResultInner(value: object): boolean {
|
||||||
|
let isInstance = true;
|
||||||
|
|
||||||
|
return isInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerFromJSON(json: any): RequestsPerSecondSchemaDataResultInner {
|
||||||
|
return RequestsPerSecondSchemaDataResultInnerFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaDataResultInner {
|
||||||
|
if ((json === undefined) || (json === null)) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'metric': !exists(json, 'metric') ? undefined : RequestsPerSecondSchemaDataResultInnerMetricFromJSON(json['metric']),
|
||||||
|
'values': !exists(json, 'values') ? undefined : json['values'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerToJSON(value?: RequestsPerSecondSchemaDataResultInner | null): any {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'metric': RequestsPerSecondSchemaDataResultInnerMetricToJSON(value.metric),
|
||||||
|
'values': value.values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Unleash API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 4.19.0-beta.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exists, mapValues } from '../runtime';
|
||||||
|
/**
|
||||||
|
* A key value set representing the metric
|
||||||
|
* @export
|
||||||
|
* @interface RequestsPerSecondSchemaDataResultInnerMetric
|
||||||
|
*/
|
||||||
|
export interface RequestsPerSecondSchemaDataResultInnerMetric {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof RequestsPerSecondSchemaDataResultInnerMetric
|
||||||
|
*/
|
||||||
|
appName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the RequestsPerSecondSchemaDataResultInnerMetric interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfRequestsPerSecondSchemaDataResultInnerMetric(value: object): boolean {
|
||||||
|
let isInstance = true;
|
||||||
|
|
||||||
|
return isInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerMetricFromJSON(json: any): RequestsPerSecondSchemaDataResultInnerMetric {
|
||||||
|
return RequestsPerSecondSchemaDataResultInnerMetricFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerMetricFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaDataResultInnerMetric {
|
||||||
|
if ((json === undefined) || (json === null)) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'appName': !exists(json, 'appName') ? undefined : json['appName'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerMetricToJSON(value?: RequestsPerSecondSchemaDataResultInnerMetric | null): any {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'appName': value.appName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Unleash API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 4.19.0-beta.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exists, mapValues } from '../runtime';
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface RequestsPerSecondSchemaDataResultInnerValuesInnerInner
|
||||||
|
*/
|
||||||
|
export type RequestsPerSecondSchemaDataResultInnerValuesInnerInner = number | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the RequestsPerSecondSchemaDataResultInnerValuesInnerInner interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfRequestsPerSecondSchemaDataResultInnerValuesInnerInner(value: object): boolean {
|
||||||
|
let isInstance = true;
|
||||||
|
|
||||||
|
return isInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSON(json: any): RequestsPerSecondSchemaDataResultInnerValuesInnerInner {
|
||||||
|
return RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaDataResultInnerValuesInnerInner {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSchemaDataResultInnerValuesInnerInnerToJSON(value?: RequestsPerSecondSchemaDataResultInnerValuesInnerInner | null): any {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Unleash API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 4.19.0-beta.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exists, mapValues } from '../runtime';
|
||||||
|
import type { RequestsPerSecondSchema } from './RequestsPerSecondSchema';
|
||||||
|
import {
|
||||||
|
RequestsPerSecondSchemaFromJSON,
|
||||||
|
RequestsPerSecondSchemaFromJSONTyped,
|
||||||
|
RequestsPerSecondSchemaToJSON,
|
||||||
|
} from './RequestsPerSecondSchema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface RequestsPerSecondSegmentedSchema
|
||||||
|
*/
|
||||||
|
export interface RequestsPerSecondSegmentedSchema {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {RequestsPerSecondSchema}
|
||||||
|
* @memberof RequestsPerSecondSegmentedSchema
|
||||||
|
*/
|
||||||
|
clientMetrics?: RequestsPerSecondSchema;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {RequestsPerSecondSchema}
|
||||||
|
* @memberof RequestsPerSecondSegmentedSchema
|
||||||
|
*/
|
||||||
|
adminMetrics?: RequestsPerSecondSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the RequestsPerSecondSegmentedSchema interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfRequestsPerSecondSegmentedSchema(value: object): boolean {
|
||||||
|
let isInstance = true;
|
||||||
|
|
||||||
|
return isInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSegmentedSchemaFromJSON(json: any): RequestsPerSecondSegmentedSchema {
|
||||||
|
return RequestsPerSecondSegmentedSchemaFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSegmentedSchemaFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSegmentedSchema {
|
||||||
|
if ((json === undefined) || (json === null)) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'clientMetrics': !exists(json, 'clientMetrics') ? undefined : RequestsPerSecondSchemaFromJSON(json['clientMetrics']),
|
||||||
|
'adminMetrics': !exists(json, 'adminMetrics') ? undefined : RequestsPerSecondSchemaFromJSON(json['adminMetrics']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestsPerSecondSegmentedSchemaToJSON(value?: RequestsPerSecondSegmentedSchema | null): any {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
|
||||||
|
'clientMetrics': RequestsPerSecondSchemaToJSON(value.clientMetrics),
|
||||||
|
'adminMetrics': RequestsPerSecondSchemaToJSON(value.adminMetrics),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -17,3 +17,5 @@ export * from './UpdateFeatureSchema';
|
|||||||
export * from './UpdateStrategySchema';
|
export * from './UpdateStrategySchema';
|
||||||
export * from './VariantSchema';
|
export * from './VariantSchema';
|
||||||
export * from './VariantSchemaPayload';
|
export * from './VariantSchemaPayload';
|
||||||
|
export * from './RequestsPerSecondSchema';
|
||||||
|
export * from './RequestsPerSecondSchemaDataResultInnerValuesInnerInner';
|
||||||
|
14
frontend/src/utils/cyclicIterator.test.ts
Normal file
14
frontend/src/utils/cyclicIterator.test.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CyclicIterator } from './cyclicIterator';
|
||||||
|
|
||||||
|
test('loops around the list', () => {
|
||||||
|
const iterator = new CyclicIterator<number>([1, 3, 5, 7]);
|
||||||
|
expect(iterator.next()).toEqual(1);
|
||||||
|
expect(iterator.next()).toEqual(3);
|
||||||
|
expect(iterator.next()).toEqual(5);
|
||||||
|
expect(iterator.next()).toEqual(7);
|
||||||
|
expect(iterator.next()).toEqual(1);
|
||||||
|
expect(iterator.next()).toEqual(3);
|
||||||
|
expect(iterator.next()).toEqual(5);
|
||||||
|
expect(iterator.next()).toEqual(7);
|
||||||
|
expect(iterator.next()).toEqual(1);
|
||||||
|
});
|
12
frontend/src/utils/cyclicIterator.ts
Normal file
12
frontend/src/utils/cyclicIterator.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export class CyclicIterator<T> {
|
||||||
|
private current = 0;
|
||||||
|
readonly all: T[];
|
||||||
|
constructor(defaultList: T[]) {
|
||||||
|
this.all = defaultList;
|
||||||
|
}
|
||||||
|
next(): T {
|
||||||
|
const item = this.all[this.current];
|
||||||
|
this.current = (this.current + 1) % this.all.length;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
@ -201,7 +201,8 @@ class MetricsController extends Controller {
|
|||||||
clientMetrics,
|
clientMetrics,
|
||||||
adminMetrics,
|
adminMetrics,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
|
this.logger.error('Failed to fetch RPS metrics', err);
|
||||||
res.status(500).send('Error fetching RPS metrics');
|
res.status(500).send('Error fetching RPS metrics');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user