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>
|
||||
}
|
||||
/>
|
||||
{flags.networkView && (
|
||||
<Tab
|
||||
value="/admin/traffic"
|
||||
label={
|
||||
<CenteredNavLink to="/admin/traffic">
|
||||
Traffic
|
||||
</CenteredNavLink>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isBilling && (
|
||||
<Tab
|
||||
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",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "networkView",
|
||||
"menu": {
|
||||
"adminSettings": true,
|
||||
},
|
||||
"parent": "/admin",
|
||||
"path": "/admin/traffic",
|
||||
"title": "Traffic",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "embedProxyFrontend",
|
||||
|
@ -60,6 +60,7 @@ import { CorsAdmin } from 'component/admin/cors';
|
||||
import { InviteLink } from 'component/admin/users/InviteLink/InviteLink';
|
||||
import { Profile } from 'component/user/Profile/Profile';
|
||||
import { InstanceAdmin } from '../admin/instance-admin/InstanceAdmin';
|
||||
import { Traffic } from 'component/admin/traffic/Traffic';
|
||||
|
||||
export const routes: IRoute[] = [
|
||||
// Splash
|
||||
@ -509,6 +510,15 @@ export const routes: IRoute[] = [
|
||||
type: 'protected',
|
||||
menu: { adminSettings: true },
|
||||
},
|
||||
{
|
||||
path: '/admin/traffic',
|
||||
parent: '/admin',
|
||||
title: 'Traffic',
|
||||
component: Traffic,
|
||||
type: 'protected',
|
||||
menu: { adminSettings: true },
|
||||
flag: 'networkView',
|
||||
},
|
||||
{
|
||||
path: '/admin/cors',
|
||||
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 './VariantSchema';
|
||||
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,
|
||||
adminMetrics,
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to fetch RPS metrics', err);
|
||||
res.status(500).send('Error fetching RPS metrics');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user