Merge pull request #6 from himanshu-source21/main

Fix Sig-13, Fix-11, Logo and lint
This commit is contained in:
Ankit Nayan 2021-01-20 11:48:45 +05:30 committed by GitHub
commit cd16bf43bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 53826 additions and 3350 deletions

4
.gitignore vendored
View File

@ -19,9 +19,13 @@ frontend/.yarnclean
frontend/npm-debug.log*
frontend/yarn-debug.log*
frontend/yarn-error.log*
frontend/src/constants/env.ts
.idea
**/.vscode
*.tgz
**/build
**/storage
**/locust-scripts/__pycache__/

View File

@ -0,0 +1,7 @@
{
"trailingComma": "all",
"useTabs": true,
"tabWidth": 1,
"singleQuote": false,
"jsxSingleQuote": false
}

View File

@ -1,19 +1,18 @@
# stage1 as builder
FROM node:14-alpine as builder
WORKDIR /frontend
# copy the package.json to install dependencies
COPY package.json ./
# Install the dependencies and make the folder
RUN npm install && mkdir /react-ui && mv ./node_modules ./react-ui
WORKDIR /react-ui
RUN yarn install
COPY . .
# Build the project and copy the files
RUN npm run build
RUN yarn build
FROM nginx:1.15-alpine
@ -25,7 +24,7 @@ COPY conf/default.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
# Copy from the stahg 1
COPY --from=builder /react-ui/build /usr/share/nginx/html
COPY --from=builder /frontend/build /usr/share/nginx/html
EXPOSE 3000

View File

@ -1,3 +1,27 @@
# Docker
**Building image**
```docker-compose up``
/ This will also run
or
```docker build . -t tagname```
**Tag to remote url- Introduce versinoing later on**
```
docker tag signoz/frontend:latest 7296823551/signoz:latest
```
**Running locally**
```
docker-compose up
```
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

View File

@ -0,0 +1,7 @@
version: "3.9"
services:
web:
build: .
image: signoz/frontend:latest
ports:
- "3000:3000"

View File

@ -1,28 +1,28 @@
const gulp = require('gulp')
const gulpless = require('gulp-less')
const postcss = require('gulp-postcss')
const debug = require('gulp-debug')
var csso = require('gulp-csso')
const autoprefixer = require('autoprefixer')
const NpmImportPlugin = require('less-plugin-npm-import')
const gulp = require("gulp");
const gulpless = require("gulp-less");
const postcss = require("gulp-postcss");
const debug = require("gulp-debug");
var csso = require("gulp-csso");
const autoprefixer = require("autoprefixer");
const NpmImportPlugin = require("less-plugin-npm-import");
gulp.task('less', function () {
const plugins = [autoprefixer()]
gulp.task("less", function () {
const plugins = [autoprefixer()];
return gulp
.src('src/themes/*-theme.less')
.pipe(debug({title: 'Less files:'}))
.pipe(
gulpless({
javascriptEnabled: true,
plugins: [new NpmImportPlugin({prefix: '~'})],
}),
)
.pipe(postcss(plugins))
.pipe(
csso({
debug: true,
}),
)
.pipe(gulp.dest('./public'))
})
return gulp
.src("src/themes/*-theme.less")
.pipe(debug({ title: "Less files:" }))
.pipe(
gulpless({
javascriptEnabled: true,
plugins: [new NpmImportPlugin({ prefix: "~" })],
}),
)
.pipe(postcss(plugins))
.pipe(
csso({
debug: true,
}),
)
.pipe(gulp.dest("./public"));
});

View File

@ -1,87 +1,91 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.0.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/chart.js": "^2.9.28",
"@types/d3": "^6.2.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.7",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/redux": "^3.6.0",
"@types/styled-components": "^5.1.4",
"@types/vis": "^4.21.21",
"antd": "^4.8.0",
"axios": "^0.21.0",
"chart.js": "^2.9.4",
"d3": "^6.2.0",
"d3-array": "^2.8.0",
"d3-ease": "^2.0.0",
"d3-flame-graph": "^3.1.1",
"d3-tip": "^0.9.1",
"material-ui-chip-input": "^2.0.0-beta.2",
"prop-types": "^15.6.2",
"react": "17.0.0",
"react-chartjs-2": "^2.11.1",
"react-chips": "^0.8.0",
"react-css-theme-switcher": "^0.1.6",
"react-dom": "17.0.0",
"react-graph-vis": "^1.0.5",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.0",
"react-vis": "^1.11.7",
"recharts": "^1.8.5",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"styled-components": "^5.2.1",
"typescript": "^4.0.5",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"storybook": "start-storybook -p 6006 -s public --no-dll",
"build-storybook": "build-storybook -s public --no-dll"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/preset-typescript": "^7.12.7",
"autoprefixer": "^9.0.0",
"babel-plugin-styled-components": "^1.12.0",
"gulp": "^4.0.2",
"gulp-csso": "^4.0.1",
"gulp-debug": "^4.0.0",
"gulp-less": "^4.0.1",
"gulp-postcss": "^9.0.0",
"less-plugin-npm-import": "^2.1.0",
"react-is": "^17.0.1"
}
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.0.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/chart.js": "^2.9.28",
"@types/d3": "^6.2.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.7",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/redux": "^3.6.0",
"@types/styled-components": "^5.1.4",
"@types/vis": "^4.21.21",
"antd": "^4.8.0",
"axios": "^0.21.0",
"chart.js": "^2.9.4",
"d3": "^6.2.0",
"d3-array": "^2.8.0",
"d3-ease": "^2.0.0",
"d3-flame-graph": "^3.1.1",
"d3-tip": "^0.9.1",
"material-ui-chip-input": "^2.0.0-beta.2",
"prop-types": "^15.6.2",
"react": "17.0.0",
"react-chartjs-2": "^2.11.1",
"react-chips": "^0.8.0",
"react-css-theme-switcher": "^0.1.6",
"react-dom": "17.0.0",
"react-graph-vis": "^1.0.5",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.0",
"react-vis": "^1.11.7",
"recharts": "^1.8.5",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"styled-components": "^5.2.1",
"typescript": "^4.0.5",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"storybook": "start-storybook -p 6006 -s public --no-dll",
"build-storybook": "build-storybook -s public --no-dll",
"prettify": "prettier --write ."
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/preset-typescript": "^7.12.7",
"autoprefixer": "^9.0.0",
"babel-plugin-styled-components": "^1.12.0",
"gulp": "^4.0.2",
"gulp-csso": "^4.0.1",
"gulp-debug": "^4.0.0",
"gulp-less": "^4.0.1",
"gulp-postcss": "^9.0.0",
"husky": "4.3.8",
"less-plugin-npm-import": "^2.1.0",
"lint-staged": "10.5.3",
"prettier": "2.2.1",
"react-is": "^17.0.1"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,22 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!--
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
@ -25,12 +27,12 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
@ -40,5 +42,5 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,25 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,75 +1,71 @@
import { ActionTypes } from './types';
import { Moment } from 'moment'
export type DateTimeRangeType = [Moment|null,Moment|null]|null;
import { ActionTypes } from "./types";
import { Moment } from "moment";
export type DateTimeRangeType = [Moment | null, Moment | null] | null;
export interface GlobalTime {
maxTime: number;
minTime: number;
maxTime: number;
minTime: number;
}
export interface updateTimeIntervalAction {
type: ActionTypes.updateTimeInterval;
payload: GlobalTime;
}
type: ActionTypes.updateTimeInterval;
payload: GlobalTime;
}
export const updateTimeInterval = (interval:string, datetimeRange?:[number,number]) => {
export const updateTimeInterval = (
interval: string,
datetimeRange?: [number, number],
) => {
let maxTime: number = 0;
let minTime: number = 0;
// if interval string is custom, then datetimRange should be present and max & min time should be
// set directly based on that. Assuming datetimeRange values are in ms, and minTime is 0th element
let maxTime: number = 0;
let minTime: number = 0;
// if interval string is custom, then datetimRange should be present and max & min time should be
// set directly based on that. Assuming datetimeRange values are in ms, and minTime is 0th element
switch (interval) {
case "15min":
maxTime = Date.now() * 1000000; // in nano sec
minTime = (Date.now() - 15 * 60 * 1000) * 1000000;
break;
switch (interval) {
case '15min':
case "30min":
maxTime = Date.now() * 1000000; // in nano sec
minTime = (Date.now() - 30 * 60 * 1000) * 1000000;
break;
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-15*60*1000)*1000000;
break;
case "1hr":
maxTime = Date.now() * 1000000; // in nano sec
minTime = (Date.now() - 1 * 60 * 60 * 1000) * 1000000;
break;
case '30min':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-30*60*1000)*1000000;
break;
case "6hr":
maxTime = Date.now() * 1000000; // in nano sec
minTime = (Date.now() - 6 * 60 * 60 * 1000) * 1000000;
break;
case '1hr':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-1*60*60*1000)*1000000;
break;
case "1day":
maxTime = Date.now() * 1000000; // in nano sec
minTime = (Date.now() - 24 * 60 * 60 * 1000) * 1000000;
break;
case '6hr':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-6*60*60*1000)*1000000;
break;
case "1week":
maxTime = Date.now() * 1000000; // in nano sec
minTime = (Date.now() - 7 * 24 * 60 * 60 * 1000) * 1000000;
break;
case '1day':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-24*60*60*1000)*1000000;
break;
case "custom":
if (datetimeRange !== undefined) {
maxTime = datetimeRange[1] * 1000000; // in nano sec
minTime = datetimeRange[0] * 1000000; // in nano sec
}
break;
case '1week':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-7*24*60*60*1000)*1000000;
break;
default:
console.log("not found matching case");
}
case 'custom':
if (datetimeRange !== undefined)
{
maxTime=datetimeRange[1]*1000000;// in nano sec
minTime=datetimeRange[0]*1000000;// in nano sec
}
break;
default:
console.log('not found matching case')
}
return {
type: ActionTypes.updateTimeInterval,
payload: {maxTime:maxTime, minTime:minTime},
};
return {
type: ActionTypes.updateTimeInterval,
payload: { maxTime: maxTime, minTime: minTime },
};
};

View File

@ -1,6 +1,6 @@
export * from './types';
export * from './traceFilters';
export * from './traces';
export * from './metrics';
export * from './usage';
export * from './global';
export * from "./types";
export * from "./traceFilters";
export * from "./traces";
export * from "./metrics";
export * from "./usage";
export * from "./global";

View File

@ -1,112 +1,139 @@
import { Dispatch } from 'redux';
import metricsAPI from '../api/metricsAPI';
import { GlobalTime } from './global';
import { ActionTypes } from './types';
import { Dispatch } from "redux";
import metricsAPI from "../api/metricsAPI";
import { GlobalTime } from "./global";
import { ActionTypes } from "./types";
export interface servicesListItem{
"serviceName": string;
"p99": number;
"avgDuration": number;
"numCalls": number;
"callRate": number;
"numErrors": number;
"errorRate": number;
};
export interface metricItem{
"timestamp":number;
"p50":number;
"p90":number;
"p99":number;
"numCalls":number;
"callRate":number;
"numErrors":number;
"errorRate":number;
export interface servicesListItem {
serviceName: string;
p99: number;
avgDuration: number;
numCalls: number;
callRate: number;
numErrors: number;
errorRate: number;
}
export interface topEndpointListItem{
"p50": number;
"p90": number;
"p99": number;
"numCalls": number;
"name": string;
};
export interface metricItem {
timestamp: number;
p50: number;
p90: number;
p99: number;
numCalls: number;
callRate: number;
numErrors: number;
errorRate: number;
}
export interface customMetricsItem{
"timestamp": number;
"value": number;
};
export interface topEndpointListItem {
p50: number;
p90: number;
p99: number;
numCalls: number;
name: string;
}
export interface customMetricsItem {
timestamp: number;
value: number;
}
export interface getServicesListAction {
type: ActionTypes.getServicesList;
payload: servicesListItem[];
}
type: ActionTypes.getServicesList;
payload: servicesListItem[];
}
export interface getServiceMetricsAction{
type: ActionTypes.getServiceMetrics;
payload: metricItem[];
export interface getServiceMetricsAction {
type: ActionTypes.getServiceMetrics;
payload: metricItem[];
}
export interface getTopEndpointsAction {
type: ActionTypes.getTopEndpoints;
payload: topEndpointListItem[];
type: ActionTypes.getTopEndpoints;
payload: topEndpointListItem[];
}
export interface getFilteredTraceMetricsAction{
type: ActionTypes.getFilteredTraceMetrics;
payload: customMetricsItem[];
export interface getFilteredTraceMetricsAction {
type: ActionTypes.getFilteredTraceMetrics;
payload: customMetricsItem[];
}
export const getServicesList = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'services?start='+globalTime.minTime+'&end='+globalTime.maxTime;
const response = await metricsAPI.get<servicesListItem[]>(request_string);
return async (dispatch: Dispatch) => {
let request_string =
"services?start=" + globalTime.minTime + "&end=" + globalTime.maxTime;
const response = await metricsAPI.get<servicesListItem[]>(request_string);
dispatch<getServicesListAction>({
type: ActionTypes.getServicesList,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
dispatch<getServicesListAction>({
type: ActionTypes.getServicesList,
payload: response.data,
//PNOTE - response.data in the axios response has the actual API response
});
};
};
export const getServicesMetrics = (serviceName:string, globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'service/overview?service='+serviceName+'&start='+globalTime.minTime+'&end='+globalTime.maxTime+'&step=60';
const response = await metricsAPI.get<metricItem[]>(request_string);
export const getServicesMetrics = (
serviceName: string,
globalTime: GlobalTime,
) => {
return async (dispatch: Dispatch) => {
let request_string =
"service/overview?service=" +
serviceName +
"&start=" +
globalTime.minTime +
"&end=" +
globalTime.maxTime +
"&step=60";
const response = await metricsAPI.get<metricItem[]>(request_string);
dispatch<getServiceMetricsAction>({
type: ActionTypes.getServiceMetrics,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
dispatch<getServiceMetricsAction>({
type: ActionTypes.getServiceMetrics,
payload: response.data,
//PNOTE - response.data in the axios response has the actual API response
});
};
};
export const getTopEndpoints = (serviceName:string, globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'service/top_endpoints?service='+serviceName+'&start='+globalTime.minTime+'&end='+globalTime.maxTime;
const response = await metricsAPI.get<topEndpointListItem[]>(request_string);
export const getTopEndpoints = (
serviceName: string,
globalTime: GlobalTime,
) => {
return async (dispatch: Dispatch) => {
let request_string =
"service/top_endpoints?service=" +
serviceName +
"&start=" +
globalTime.minTime +
"&end=" +
globalTime.maxTime;
const response = await metricsAPI.get<topEndpointListItem[]>(request_string);
dispatch<getTopEndpointsAction>({
type: ActionTypes.getTopEndpoints,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
dispatch<getTopEndpointsAction>({
type: ActionTypes.getTopEndpoints,
payload: response.data,
//PNOTE - response.data in the axios response has the actual API response
});
};
};
export const getFilteredTraceMetrics = (filter_params: string, globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'spans/aggregates?start='+globalTime.minTime+'&end='+globalTime.maxTime+'&'+filter_params;
const response = await metricsAPI.get<customMetricsItem[]>(request_string);
export const getFilteredTraceMetrics = (
filter_params: string,
globalTime: GlobalTime,
) => {
return async (dispatch: Dispatch) => {
let request_string =
"spans/aggregates?start=" +
globalTime.minTime +
"&end=" +
globalTime.maxTime +
"&" +
filter_params;
const response = await metricsAPI.get<customMetricsItem[]>(request_string);
dispatch<getFilteredTraceMetricsAction>({
type: ActionTypes.getFilteredTraceMetrics,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
dispatch<getFilteredTraceMetricsAction>({
type: ActionTypes.getFilteredTraceMetrics,
payload: response.data,
//PNOTE - response.data in the axios response has the actual API response
});
};
};

View File

@ -1,49 +1,47 @@
// Action creator must have a type and optionally a payload
import { ActionTypes } from './types'
import { ActionTypes } from "./types";
export interface TagItem {
key: string;
value: string;
operator: 'equals'|'contains';
key: string;
value: string;
operator: "equals" | "contains";
}
export interface LatencyValue {
min:string;
max:string;
min: string;
max: string;
}
export interface TraceFilters{
tags?: TagItem[];
service?:string;
latency?:LatencyValue;
operation?:string;
export interface TraceFilters {
tags?: TagItem[];
service?: string;
latency?: LatencyValue;
operation?: string;
}
//define interface for action. Action creator always returns object of this type
export interface updateTraceFiltersAction {
type: ActionTypes.updateTraceFilters,
payload: TraceFilters,
type: ActionTypes.updateTraceFilters;
payload: TraceFilters;
}
export const updateTraceFilters = (traceFilters: TraceFilters) => {
return {
type: ActionTypes.updateTraceFilters,
payload: traceFilters,
};
return {
type: ActionTypes.updateTraceFilters,
payload: traceFilters,
};
};
export interface updateInputTagAction {
type: ActionTypes.updateInput,
payload: string,
type: ActionTypes.updateInput;
payload: string;
}
export const updateInputTag = (Input: string) => {
return {
type: ActionTypes.updateInput,
payload: Input,
};
return {
type: ActionTypes.updateInput,
payload: Input,
};
};
//named export when you want to export multiple functions from the same file

View File

@ -1,8 +1,7 @@
import { ActionTypes } from './types';
import tracesAPI from '../api/tracesAPI';
import { Dispatch } from 'redux';
import { GlobalTime } from './global';
import { ActionTypes } from "./types";
import tracesAPI from "../api/tracesAPI";
import { Dispatch } from "redux";
import { GlobalTime } from "./global";
// PNOTE
// define trace interface - what it should return
@ -10,137 +9,145 @@ import { GlobalTime } from './global';
// Date() - takes number of milliseconds as input, our API takes in microseconds
// Sample API call for traces - https://api.signoz.io/api/traces?end=1606968273667000&limit=20&lookback=2d&maxDuration=&minDuration=&service=driver&operation=&start=1606968100867000
export interface Tree{
name: string;
value: number;
children?: Tree[];
export interface Tree {
name: string;
value: number;
children?: Tree[];
}
export interface RefItem{
refType: string;
traceID: string;
spanID: string;
export interface RefItem {
refType: string;
traceID: string;
spanID: string;
}
export interface TraceTagItem{
key: string;
// type: string;
value: string;
export interface TraceTagItem {
key: string;
// type: string;
value: string;
}
export interface ProcessItem{
serviceName: string;
tags: TraceTagItem[];
export interface ProcessItem {
serviceName: string;
tags: TraceTagItem[];
}
// PNOTE - Temp DS for converting span to tree which can be consumed by flamegraph
export interface pushDStree {
id: string;
name: string;
value: number;
time: number;
startTime: number;
tags: TraceTagItem[];
children: pushDStree[];
id: string;
name: string;
value: number;
time: number;
startTime: number;
tags: TraceTagItem[];
children: pushDStree[];
}
export interface spanItem{
traceID: string; // index 0
spanID: string; // index 1
operationName: string; // index 2
startTime: number; // index 3
duration: number; // index 4
references: RefItem[]; // index 5
tags: []; //index 6
logs: []; // index 7
processID: string; // index 8
warnings: []; // index 9
children: pushDStree[]; // index 10 // PNOTE - added this as we are adding extra field in span item to convert trace to tree.
// Should this field be optional?
export interface spanItem {
traceID: string; // index 0
spanID: string; // index 1
operationName: string; // index 2
startTime: number; // index 3
duration: number; // index 4
references: RefItem[]; // index 5
tags: []; //index 6
logs: []; // index 7
processID: string; // index 8
warnings: []; // index 9
children: pushDStree[]; // index 10 // PNOTE - added this as we are adding extra field in span item to convert trace to tree.
// Should this field be optional?
}
//let mapped_array :{ [id: string] : spanItem; } = {};
export interface traceItem{
traceID: string;
spans: spanItem[];
processes: { [id: string] : ProcessItem; } ;
warnings: [];
export interface traceItem {
traceID: string;
spans: spanItem[];
processes: { [id: string]: ProcessItem };
warnings: [];
}
export interface traceResponse{
data: traceItem[];
total: number;
limit: number;
offset: number;
error: [];
export interface traceResponse {
data: traceItem[];
total: number;
limit: number;
offset: number;
error: [];
}
export type span = [number, string, string, string, string, string, string, string|string[], string|string[], string|string[], pushDStree[]];
export interface spanList{
events: span[];
segmentID: string;
columns: string[];
export type span = [
number,
string,
string,
string,
string,
string,
string,
string | string[],
string | string[],
string | string[],
pushDStree[],
];
export interface spanList {
events: span[];
segmentID: string;
columns: string[];
}
// export interface traceResponseNew{
// [id: string] : spanList;
// }
export interface traceResponseNew{
[id: string] : spanList;
export interface traceResponseNew {
[id: string]: spanList;
}
export interface spansWSameTraceIDResponse{
[id: string] : spanList;
export interface spansWSameTraceIDResponse {
[id: string]: spanList;
}
export interface FetchTracesAction {
type: ActionTypes.fetchTraces;
payload: traceResponseNew;
}
export interface FetchTraceItemAction {
type: ActionTypes.fetchTraceItem;
payload: spansWSameTraceIDResponse;
type: ActionTypes.fetchTraces;
payload: traceResponseNew;
}
export const fetchTraces = (globalTime: GlobalTime, filter_params: string ) => {
return async (dispatch: Dispatch) => {
if (globalTime){
let request_string = 'spans?limit=100&lookback=2d&start='+globalTime.minTime+'&end='+globalTime.maxTime+'&'+filter_params;
const response = await tracesAPI.get<traceResponseNew>(request_string);
export interface FetchTraceItemAction {
type: ActionTypes.fetchTraceItem;
payload: spansWSameTraceIDResponse;
}
dispatch<FetchTracesAction>({
type: ActionTypes.fetchTraces,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response?
});
export const fetchTraces = (globalTime: GlobalTime, filter_params: string) => {
return async (dispatch: Dispatch) => {
if (globalTime) {
let request_string =
"spans?limit=100&lookback=2d&start=" +
globalTime.minTime +
"&end=" +
globalTime.maxTime +
"&" +
filter_params;
const response = await tracesAPI.get<traceResponseNew>(request_string);
}
};
};
export const fetchTraceItem = (traceID: string) => {
return async (dispatch: Dispatch) => {
let request_string = 'traces/'+traceID;
const response = await tracesAPI.get<spansWSameTraceIDResponse>(request_string);
dispatch<FetchTraceItemAction>({
type: ActionTypes.fetchTraceItem,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response?
});
};
};
dispatch<FetchTracesAction>({
type: ActionTypes.fetchTraces,
payload: response.data,
//PNOTE - response.data in the axios response has the actual API response?
});
}
};
};
export const fetchTraceItem = (traceID: string) => {
return async (dispatch: Dispatch) => {
let request_string = "traces/" + traceID;
const response = await tracesAPI.get<spansWSameTraceIDResponse>(
request_string,
);
dispatch<FetchTraceItemAction>({
type: ActionTypes.fetchTraceItem,
payload: response.data,
//PNOTE - response.data in the axios response has the actual API response?
});
};
};

View File

@ -1,22 +1,35 @@
import { FetchTracesAction, FetchTraceItemAction } from './traces';
import { updateTraceFiltersAction, updateInputTagAction } from './traceFilters';
import {getServicesListAction,getServiceMetricsAction, getTopEndpointsAction, getFilteredTraceMetricsAction} from './metrics'
import {getUsageDataAction} from './usage'
import {updateTimeIntervalAction} from './global';
import { FetchTracesAction, FetchTraceItemAction } from "./traces";
import { updateTraceFiltersAction, updateInputTagAction } from "./traceFilters";
import {
getServicesListAction,
getServiceMetricsAction,
getTopEndpointsAction,
getFilteredTraceMetricsAction,
} from "./metrics";
import { getUsageDataAction } from "./usage";
import { updateTimeIntervalAction } from "./global";
export enum ActionTypes {
updateTraceFilters,
updateInput,
fetchTraces,
fetchTraceItem,
getServicesList,
getServiceMetrics,
getTopEndpoints,
getUsageData,
updateTimeInterval,
getFilteredTraceMetrics,
updateTraceFilters,
updateInput,
fetchTraces,
fetchTraceItem,
getServicesList,
getServiceMetrics,
getTopEndpoints,
getUsageData,
updateTimeInterval,
getFilteredTraceMetrics,
}
export type Action = FetchTraceItemAction | FetchTracesAction | updateTraceFiltersAction | updateInputTagAction |getServicesListAction| getServiceMetricsAction| getTopEndpointsAction | getUsageDataAction | updateTimeIntervalAction| getFilteredTraceMetricsAction;
export type Action =
| FetchTraceItemAction
| FetchTracesAction
| updateTraceFiltersAction
| updateInputTagAction
| getServicesListAction
| getServiceMetricsAction
| getTopEndpointsAction
| getUsageDataAction
| updateTimeIntervalAction
| getFilteredTraceMetricsAction;

View File

@ -1,29 +1,33 @@
import { Dispatch } from 'redux';
import metricsAPI from '../api/metricsAPI';
import { ActionTypes } from './types';
import { GlobalTime } from './global';
import { Dispatch } from "redux";
import metricsAPI from "../api/metricsAPI";
import { ActionTypes } from "./types";
import { GlobalTime } from "./global";
export interface usageDataItem{
"timestamp":number;
"count":number;
}
export interface usageDataItem {
timestamp: number;
count: number;
}
export interface getUsageDataAction {
type: ActionTypes.getUsageData;
payload: usageDataItem[];
type: ActionTypes.getUsageData;
payload: usageDataItem[];
}
export const getUsageData = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'usage?start='+globalTime.minTime+'&end='+globalTime.maxTime+'&step=3600&service=driver';
//Step can only be multiple of 3600
const response = await metricsAPI.get<usageDataItem[]>(request_string);
return async (dispatch: Dispatch) => {
let request_string =
"usage?start=" +
globalTime.minTime +
"&end=" +
globalTime.maxTime +
"&step=3600&service=driver";
//Step can only be multiple of 3600
const response = await metricsAPI.get<usageDataItem[]>(request_string);
dispatch<getUsageDataAction>({
type: ActionTypes.getUsageData,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
dispatch<getUsageDataAction>({
type: ActionTypes.getUsageData,
payload: response.data,
//PNOTE - response.data in the axios response has the actual API response
});
};
};

View File

@ -1,9 +1,7 @@
import axios from 'axios';
import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
// No auth for the API
export default axios.create({
baseURL: 'https://api.signoz.io/api/prom/api/v1',
}
);
baseURL: `${ENVIRONMENT.baseURL}/api/prom/api/v1`,
});

View File

@ -1,10 +1,6 @@
import axios from 'axios';
import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
export default axios.create({
// baseURL: 'http://104.211.113.204:8080/api/v1/',
// baseURL: process.env.REACT_APP_QUERY_SERVICE_URL,
// console.log('in metrics API', process.env.QUERY_SERVICE_URL)
baseURL: '/api/v1/',
}
);
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`,
});

View File

@ -1,14 +1,12 @@
import axios from 'axios';
import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
export default axios.create({
// baseURL: 'https://api.telegram.org/bot1518273960:AAHcgVvym9a0Qkl-PKiCI84X1VZaVbkTud0/',
// baseURL: 'http://104.211.113.204:8080/api/v1/',
baseURL: '/api/v1/',
}
);
// baseURL: 'https://api.telegram.org/bot1518273960:AAHcgVvym9a0Qkl-PKiCI84X1VZaVbkTud0/',
// baseURL: 'http://104.211.113.204:8080/api/v1/',
// baseURL: "/api/v1/",
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`,
});
//https://api.telegram.org/bot1518273960:AAHcgVvym9a0Qkl-PKiCI84X1VZaVbkTud0/sendMessage?chat_id=351813222&text=Hello%20there

View File

@ -1,12 +1,11 @@
import axios from 'axios';
import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
//import { format } from 'path';
export default axios.create({
// baseURL: 'http://104.211.113.204:8080/api/v1/' //comment this line and remove this comment before pushing
// baseURL: process.env.QUERY_SERVICE_URL,
// console.log('in traces API', process.env.QUERY_SERVICE_URL)
baseURL: '/api/v1/',
// baseURL: 'http://104.211.113.204:8080/api/v1/' //comment this line and remove this comment before pushing
// baseURL: process.env.QUERY_SERVICE_URL,
// console.log('in traces API', process.env.QUERY_SERVICE_URL)
// baseURL: "/api/v1/",
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`,
});

View File

@ -1,122 +1,153 @@
import React, { Suspense, useState } from 'react';
import { Layout, Menu, Switch as ToggleButton, Spin, Row, Col } from 'antd';
import React, { Suspense, useState } from "react";
import { Layout, Menu, Switch as ToggleButton, Spin, Row, Col } from "antd";
import { useThemeSwitcher } from "react-css-theme-switcher";
import { NavLink, BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import {
NavLink,
BrowserRouter as Router,
Route,
Switch,
} from "react-router-dom";
import {
LineChartOutlined,
BarChartOutlined,
DeploymentUnitOutlined,
AlignLeftOutlined,
} from "@ant-design/icons";
LineChartOutlined,
BarChartOutlined,
DeploymentUnitOutlined,
AlignLeftOutlined,
} from '@ant-design/icons';
import DateTimeSelector from './DateTimeSelector';
import ShowBreadcrumbs from './ShowBreadcrumbs';
import DateTimeSelector from "./DateTimeSelector";
import ShowBreadcrumbs from "./ShowBreadcrumbs";
import styled from "styled-components";
const { Content, Footer, Sider } = Layout;
const ServiceMetrics = React.lazy(() => import('./metrics/ServiceMetricsDef'));
const ServiceMap = React.lazy(() => import('./servicemap/ServiceMap'));
const TraceDetail = React.lazy(() => import('./traces/TraceDetail'));
const TraceGraph = React.lazy(() => import ('./traces/TraceGraphDef' ));
const UsageExplorer = React.lazy(() => import ('./usage/UsageExplorerDef' ));
const ServicesTable = React.lazy(() => import('./metrics/ServicesTableDef'));
const ServiceMetrics = React.lazy(() => import("./metrics/ServiceMetricsDef"));
const ServiceMap = React.lazy(() => import("./servicemap/ServiceMap"));
const TraceDetail = React.lazy(() => import("./traces/TraceDetail"));
const TraceGraph = React.lazy(() => import("./traces/TraceGraphDef"));
const UsageExplorer = React.lazy(() => import("./usage/UsageExplorerDef"));
const ServicesTable = React.lazy(() => import("./metrics/ServicesTableDef"));
// const Signup = React.lazy(() => import('./Signup'));
//PNOTE
//React. lazy currently only supports default exports. If the module you want to import uses named exports, you can create an intermediate module that reexports it as the default. This ensures that tree shaking keeps working and that you don't pull in unused components.
const App = () => {
// state = { collapsed: false, isDarkMode: true };
const { switcher, currentTheme, status, themes } = useThemeSwitcher();
const [isDarkMode, setIsDarkMode] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const ThemeSwitcherWrapper = styled.div`
display: flex;
justify-content: center;
margin-top: 20px;
`;
const App = () => {
// state = { collapsed: false, isDarkMode: true };
const { switcher, currentTheme, status, themes } = useThemeSwitcher();
const [isDarkMode, setIsDarkMode] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const toggleTheme = (isChecked :boolean) => {
setIsDarkMode(isChecked);
switcher({ theme: isChecked ? themes.dark : themes.light });
};
const toggleTheme = (isChecked: boolean) => {
setIsDarkMode(isChecked);
switcher({ theme: isChecked ? themes.dark : themes.light });
};
const onCollapse = (): void => {
setCollapsed(!collapsed);
};
const onCollapse = (): void => {
setCollapsed(!collapsed);
};
if (status === "loading") {
return null;
}
if (status === "loading") {
return null;
}
return (
<Router basename="/">
<Layout style={{ minHeight: "100vh" }}>
<Sider
collapsible
collapsed={collapsed}
onCollapse={onCollapse}
width={160}
>
<div className="logo">
<ThemeSwitcherWrapper>
<ToggleButton checked={isDarkMode} onChange={toggleTheme} />
</ThemeSwitcherWrapper>
<NavLink to="/">
<img
src={"/signoz.svg"}
alt={"SigNoz"}
style={{
margin: "5%",
width: 100,
display: !collapsed ? "block" : "none",
}}
/>
</NavLink>
</div>
return (
<Router basename="/">
<Menu theme="dark" defaultSelectedKeys={["1"]} mode="inline">
<Menu.Item key="1" icon={<BarChartOutlined />}>
<NavLink
to="/application"
style={{ fontSize: 12, textDecoration: "none" }}
>
Metrics
</NavLink>
</Menu.Item>
<Menu.Item key="2" icon={<AlignLeftOutlined />}>
<NavLink to="/traces" style={{ fontSize: 12, textDecoration: "none" }}>
Traces
</NavLink>
</Menu.Item>
<Menu.Item key="3" icon={<DeploymentUnitOutlined />}>
<NavLink
to="/service-map"
style={{ fontSize: 12, textDecoration: "none" }}
>
Service Map
</NavLink>
</Menu.Item>
<Menu.Item key="4" icon={<LineChartOutlined />}>
<NavLink
to="/usage-explorer"
style={{ fontSize: 12, textDecoration: "none" }}
>
Usage Explorer
</NavLink>
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout">
<Content style={{ margin: "0 16px" }}>
<Row>
<Col span={20}>
<ShowBreadcrumbs />
</Col>
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={160}>
<div className="logo">
<ToggleButton checked={isDarkMode} onChange={toggleTheme} />
<NavLink to='/'><img src={"signoz.svg"} alt={'SigNoz'} style={{margin: '5%', width: 100, display: !collapsed ? 'block' : 'none'}} /></NavLink>
</div>
<Col span={4}>
<DateTimeSelector />
</Col>
</Row>
<Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
<Menu.Item key="1" icon={<BarChartOutlined />}>
<NavLink to='/application' style={{fontSize: 12, textDecoration: 'none'}}>Metrics</NavLink>
</Menu.Item>
<Menu.Item key="2" icon={<AlignLeftOutlined />}>
<NavLink to='/traces' style={{fontSize: 12, textDecoration: 'none'}}>Traces</NavLink>
</Menu.Item>
<Menu.Item key="3" icon={<DeploymentUnitOutlined />}>
<NavLink to='/service-map' style={{fontSize: 12, textDecoration: 'none'}}>Service Map</NavLink>
</Menu.Item>
<Menu.Item key="4" icon={<LineChartOutlined />}>
<NavLink to='/usage-explorer' style={{fontSize: 12, textDecoration: 'none'}}>Usage Explorer</NavLink>
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout">
{/* <Divider /> */}
<Content style={{ margin: '0 16px' }}>
<Row>
<Col span={20}>
<ShowBreadcrumbs />
</Col>
<Col span={4}>
<DateTimeSelector />
</Col>
</Row>
{/* <Divider /> */}
<Suspense fallback={<Spin size="large" />}>
<Switch>
<Route path="/application/:servicename" component={ServiceMetrics}/>
<Route path="/service-map" component={ServiceMap}/>
<Route path="/traces" exact component={TraceDetail}/>
<Route path="/traces/:id" component={TraceGraph}/>
<Route path="/usage-explorer" component={UsageExplorer}/>
<Route path="/" component={ServicesTable}/>
<Route path="/application" exact component={ServicesTable}/>
{/* <Route path="/signup" component={Signup} /> */}
</Switch>
</Suspense>
</Content>
<Footer style={{ textAlign: 'center', fontSize: 10 }}>SigNoz Inc. ©2020 </Footer>
</Layout>
</Layout>
</Router>
);
}
<Suspense fallback={<Spin size="large" />}>
<Switch>
<Route path="/application/:servicename" component={ServiceMetrics} />
<Route path="/service-map" component={ServiceMap} />
<Route path="/traces" exact component={TraceDetail} />
<Route path="/traces/:id" component={TraceGraph} />
<Route path="/usage-explorer" component={UsageExplorer} />
<Route path="/" component={ServicesTable} />
<Route path="/application" exact component={ServicesTable} />
{/* <Route path="/signup" component={Signup} /> */}
</Switch>
</Suspense>
</Content>
<Footer style={{ textAlign: "center", fontSize: 10 }}>
SigNoz Inc. ©2020{" "}
</Footer>
</Layout>
</Layout>
</Router>
);
};
export default App;

View File

@ -1,53 +1,46 @@
import React,{Suspense, useState} from 'react';
import {Spin} from 'antd';
import React, { Suspense, useState } from "react";
import { Spin } from "antd";
import {
BrowserRouter as Router,
Route,
Switch,
Redirect,
} from "react-router-dom";
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
const Signup = React.lazy(() => import("./Signup"));
const App = React.lazy(() => import("./App"));
const AppWrapper = () => {
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false);
const Signup = React.lazy(() => import('./Signup'));
const App = React.lazy(() => import('./App'));
return (
<Router basename="/">
<Suspense fallback={<Spin size="large" />}>
<Switch>
<Route path="/signup" exact component={Signup} />
<Route path="/application" exact component={App} />
<Route path="/application/:servicename" component={App} />
<Route path="/service-map" component={App} />
<Route path="/traces" exact component={App} />
<Route path="/traces/:id" component={App} />
<Route path="/usage-explorer" component={App} />
const AppWrapper = () => {
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false);
return(
<Router basename="/">
<Suspense fallback={<Spin size="large" />}>
<Switch>
<Route path="/signup" exact component={Signup} />
<Route path="/application" exact component={App} />
<Route path="/application/:servicename" component={App}/>
<Route path="/service-map" component={App}/>
<Route path="/traces" exact component={App}/>
<Route path="/traces/:id" component={App}/>
<Route path="/usage-explorer" component={App}/>
<Route path="/" exact
render={() => {
return (
localStorage.getItem('isLoggedIn')==='yes' ?
<Redirect to="/application" /> :
<Redirect to="/signup" />
)
}}
/>
</Switch>
</Suspense>
</Router>
);
}
<Route
path="/"
exact
render={() => {
return localStorage.getItem("isLoggedIn") === "yes" ? (
<Redirect to="/application" />
) : (
<Redirect to="/signup" />
);
}}
/>
</Switch>
</Suspense>
</Router>
);
};
export default AppWrapper;

View File

@ -1,56 +1,58 @@
import React, { useState } from 'react';
import { Modal, DatePicker} from 'antd';
import {DateTimeRangeType} from '../actions'
import { Moment } from 'moment'
import moment from 'moment';
import React, { useState } from "react";
import { Modal, DatePicker } from "antd";
import { DateTimeRangeType } from "../actions";
import { Moment } from "moment";
import moment from "moment";
const { RangePicker } = DatePicker;
interface CustomDateTimeModalProps {
visible: boolean;
onCreate: (dateTimeRange: DateTimeRangeType) => void; //Store is defined in antd forms library
onCancel: () => void;
visible: boolean;
onCreate: (dateTimeRange: DateTimeRangeType) => void; //Store is defined in antd forms library
onCancel: () => void;
}
const CustomDateTimeModal: React.FC<CustomDateTimeModalProps> = ({ //destructuring props
visible,
onCreate,
onCancel,
const CustomDateTimeModal: React.FC<CustomDateTimeModalProps> = ({
//destructuring props
visible,
onCreate,
onCancel,
}) => {
// RangeValue<Moment> == [Moment|null,Moment|null]|null
// RangeValue<Moment> == [Moment|null,Moment|null]|null
const [
customDateTimeRange,
setCustomDateTimeRange,
] = useState<DateTimeRangeType>();
const [customDateTimeRange, setCustomDateTimeRange]=useState<DateTimeRangeType>();
function handleRangePickerOk(date_time: DateTimeRangeType) {
setCustomDateTimeRange(date_time);
}
function disabledDate(current: Moment) {
if (current > moment()) {
return true;
} else {
return false;
}
}
function handleRangePickerOk(date_time: DateTimeRangeType) {
setCustomDateTimeRange(date_time);
}
function disabledDate(current:Moment){
if (current > moment()){
return true;
}
else {
return false;
}
}
return (
<Modal
visible={visible}
title="Chose date and time range"
okText="Apply"
cancelText="Cancel"
onCancel={onCancel}
style={{ position: "absolute", top: 60, right: 40 }}
onOk={() => onCreate(customDateTimeRange?customDateTimeRange:null)}
>
<RangePicker disabledDate={disabledDate} onOk={handleRangePickerOk} showTime />
</Modal>
);
return (
<Modal
visible={visible}
title="Chose date and time range"
okText="Apply"
cancelText="Cancel"
onCancel={onCancel}
style={{ position: "absolute", top: 60, right: 40 }}
onOk={() => onCreate(customDateTimeRange ? customDateTimeRange : null)}
>
<RangePicker
disabledDate={disabledDate}
onOk={handleRangePickerOk}
showTime
/>
</Modal>
);
};
export default CustomDateTimeModal;

View File

@ -1,140 +1,169 @@
import React, { useState } from 'react';
import {Select, Button,Space, Form} from 'antd';
import styled from 'styled-components';
import React, { useEffect, useState } from "react";
import { Select, Button, Space, Form } from "antd";
import styled from "styled-components";
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import { connect } from 'react-redux';
import CustomDateTimeModal from './CustomDateTimeModal';
import { GlobalTime, updateTimeInterval } from '../actions';
import { StoreState } from '../reducers';
import FormItem from 'antd/lib/form/FormItem';
import {DateTimeRangeType} from '../actions'
import { RouteComponentProps } from "react-router-dom";
import { connect } from "react-redux";
import CustomDateTimeModal from "./CustomDateTimeModal";
import { GlobalTime, updateTimeInterval } from "../actions";
import { StoreState } from "../reducers";
import FormItem from "antd/lib/form/FormItem";
import { DateTimeRangeType } from "../actions";
import { METRICS_PAGE_QUERY_PARAM } from "../constants/query";
import { LOCAL_STORAGE } from "../constants/localStorage";
const { Option } = Select;
const DateTimeWrapper = styled.div`
margin-top:20px;
margin-top: 20px;
`;
interface DateTimeSelectorProps extends RouteComponentProps<any> {
currentpath?:string;
updateTimeInterval: Function;
globalTime: GlobalTime;
currentpath?: string;
updateTimeInterval: Function;
globalTime: GlobalTime;
}
const _DateTimeSelector = (props: DateTimeSelectorProps) => {
const defaultTime = "15min";
const [customDTPickerVisible, setCustomDTPickerVisible] = useState(false);
const [timeInterval, setTimeInterval] = useState(defaultTime);
const [refreshButtonHidden, setRefreshButtonHidden] = useState(false);
const [form_dtselector] = Form.useForm();
const _DateTimeSelector = (props:DateTimeSelectorProps) => {
useEffect(() => {
const timeDurationInLocalStorage = localStorage.getItem(
LOCAL_STORAGE.METRICS_TIME_IN_DURATION,
);
const urlParams = new URLSearchParams(window.location.search);
const timeInQueryParam = urlParams.get(METRICS_PAGE_QUERY_PARAM.time);
if (timeInQueryParam) {
setMetricsTime(timeInQueryParam);
} else if (timeDurationInLocalStorage) {
setMetricsTime(timeDurationInLocalStorage);
}
}, []);
const [customDTPickerVisible,setCustomDTPickerVisible]=useState(false);
const [timeInterval,setTimeInterval]=useState('15min')
const [refreshButtonHidden, setRefreshButtonHidden]=useState(false)
const setMetricsTime = (value: string) => {
props.history.push({
search: `?${METRICS_PAGE_QUERY_PARAM.time}=${value}`,
}); //pass time in URL query param for all choices except custom in datetime picker
props.updateTimeInterval(value);
setTimeInterval(value);
};
const [form_dtselector] = Form.useForm();
const handleOnSelect = (value: string) => {
if (value === "custom") {
setCustomDTPickerVisible(true);
} else {
setTimeInterval(value);
setRefreshButtonHidden(false); // for normal intervals, show refresh button
}
};
//function called on clicking apply in customDateTimeModal
const handleOk = (dateTimeRange: DateTimeRangeType) => {
// pass values in ms [minTime, maxTime]
if (
dateTimeRange !== null &&
dateTimeRange !== undefined &&
dateTimeRange[0] !== null &&
dateTimeRange[1] !== null
) {
props.updateTimeInterval("custom", [
dateTimeRange[0].valueOf(),
dateTimeRange[1].valueOf(),
]);
//setting globaltime
setRefreshButtonHidden(true);
form_dtselector.setFieldsValue({
interval:
dateTimeRange[0].format("YYYY/MM/DD HH:mm") +
"-" +
dateTimeRange[1].format("YYYY/MM/DD HH:mm"),
});
}
setCustomDTPickerVisible(false);
};
const handleOnSelect = (value:string) =>
{
if (value === 'custom')
{
setCustomDTPickerVisible(true);
}
else
{
props.history.push({
search: '?time='+value,
}) //pass time in URL query param for all choices except custom in datetime picker
props.updateTimeInterval(value);
setTimeInterval(value);
setRefreshButtonHidden(false); // for normal intervals, show refresh button
}
}
const timeSinceLastRefresh = () => {
let timeDiffSec = Math.round(
(Date.now() - Math.round(props.globalTime.maxTime / 1000000)) / 1000,
);
//function called on clicking apply in customDateTimeModal
const handleOk = (dateTimeRange:DateTimeRangeType) =>
{
// pass values in ms [minTime, maxTime]
if (dateTimeRange!== null && dateTimeRange!== undefined && dateTimeRange[0]!== null && dateTimeRange[1]!== null )
{
props.updateTimeInterval('custom',[dateTimeRange[0].valueOf(),dateTimeRange[1].valueOf()])
//setting globaltime
setRefreshButtonHidden(true);
form_dtselector.setFieldsValue({interval:(dateTimeRange[0].format("YYYY/MM/DD HH:mm")+'-'+dateTimeRange[1].format("YYYY/MM/DD HH:mm")) ,})
}
setCustomDTPickerVisible(false);
}
//How will Refresh button get updated? Needs to be periodically updated via timer.
// For now, not returning any text here
// if (timeDiffSec < 60)
// return timeDiffSec.toString()+' s';
// else if (timeDiffSec < 3600)
// return Math.round(timeDiffSec/60).toString()+' min';
// else
// return Math.round(timeDiffSec/3600).toString()+' hr';
return null;
};
const timeSinceLastRefresh = () => {
let timeDiffSec = Math.round((Date.now() - Math.round(props.globalTime.maxTime/1000000))/1000);
const handleRefresh = () => {
window.localStorage.setItem(
LOCAL_STORAGE.METRICS_TIME_IN_DURATION,
timeInterval,
);
setMetricsTime(timeInterval);
};
//How will Refresh button get updated? Needs to be periodically updated via timer.
// For now, not returning any text here
// if (timeDiffSec < 60)
// return timeDiffSec.toString()+' s';
// else if (timeDiffSec < 3600)
// return Math.round(timeDiffSec/60).toString()+' min';
// else
// return Math.round(timeDiffSec/3600).toString()+' hr';
return null;
const options = [
{ value: "custom", label: "Custom" },
{ value: "15min", label: "Last 15 min" },
{ value: "30min", label: "Last 30 min" },
{ value: "1hr", label: "Last 1 hour" },
{ value: "6hr", label: "Last 6 hour" },
{ value: "1day", label: "Last 1 day" },
{ value: "1week", label: "Last 1 week" },
];
if (props.location.pathname.startsWith("/usage-explorer")) {
return null;
} else {
return (
<DateTimeWrapper>
<Space>
<Form
form={form_dtselector}
layout="inline"
initialValues={{ interval: "15min" }}
style={{ marginTop: 10, marginBottom: 10 }}
>
<FormItem></FormItem>
<Select onSelect={handleOnSelect} value={timeInterval}>
{options.map(({ value, label }) => (
<Option value={value}>{label}</Option>
))}
</Select>
}
<FormItem hidden={refreshButtonHidden} name="refresh_button">
<Button type="primary" onClick={handleRefresh}>
Refresh {timeSinceLastRefresh()}
</Button>
{/* if refresh time is more than x min, give a message? */}
</FormItem>
</Form>
<CustomDateTimeModal
visible={customDTPickerVisible}
onCreate={handleOk}
onCancel={() => {
setCustomDTPickerVisible(false);
}}
/>
</Space>
</DateTimeWrapper>
);
}
};
const mapStateToProps = (state: StoreState): { globalTime: GlobalTime } => {
return { globalTime: state.globalTime };
};
const handleRefresh = () =>
{
props.updateTimeInterval(timeInterval);
}
if (props.location.pathname.startsWith('/usage-explorer')) {
return null;
} else
{
return (
<DateTimeWrapper>
<Space>
<Form form={form_dtselector} layout='inline' initialValues={{ interval:'15min', }} style={{marginTop: 10, marginBottom:10}}>
<FormItem name='interval'>
<Select onSelect={handleOnSelect} >
<Option value="custom">Custom</Option>
<Option value="15min">Last 15 min</Option>
<Option value="30min">Last 30 min</Option>
<Option value="1hr">Last 1 hour</Option>
<Option value="6hr">Last 6 hour</Option>
<Option value="1day">Last 1 day</Option>
<Option value="1week">Last 1 week</Option>
</Select>
</FormItem>
<FormItem hidden={refreshButtonHidden} name='refresh_button'>
<Button type="primary" onClick={handleRefresh}>Refresh {timeSinceLastRefresh()}</Button>
{/* if refresh time is more than x min, give a message? */}
</FormItem>
</Form>
<CustomDateTimeModal
visible={customDTPickerVisible}
onCreate={handleOk}
onCancel={() => {
setCustomDTPickerVisible(false);
}}
/>
</Space>
</DateTimeWrapper>
);
}
}
const mapStateToProps = (state: StoreState ): { globalTime: GlobalTime} => {
return { globalTime : state.globalTime };
};
export const DateTimeSelector = connect(mapStateToProps, {
updateTimeInterval: updateTimeInterval,
})(_DateTimeSelector);
export const DateTimeSelector = connect(mapStateToProps, {
updateTimeInterval: updateTimeInterval,
})(_DateTimeSelector);
export default withRouter(DateTimeSelector);

View File

@ -1,57 +1,50 @@
import React from 'react';
import {Breadcrumb} from 'antd';
import { Link, withRouter } from 'react-router-dom';
import styled from 'styled-components';
import React from "react";
import { Breadcrumb } from "antd";
import { Link, withRouter } from "react-router-dom";
import styled from "styled-components";
const BreadCrumbWrapper = styled.div`
padding-top:20px;
padding-left:20px;
padding-top: 20px;
padding-left: 20px;
`;
const breadcrumbNameMap :any = { // PNOTE - TO DO - Remove any and do typechecking - like https://stackoverflow.com/questions/56568423/typescript-no-index-signature-with-a-parameter-of-type-string-was-found-on-ty
'/application': 'Application',
'/traces': 'Traces',
'/service-map': 'Service Map',
'/usage-explorer': 'Usage Explorer',
const breadcrumbNameMap: any = {
// PNOTE - TO DO - Remove any and do typechecking - like https://stackoverflow.com/questions/56568423/typescript-no-index-signature-with-a-parameter-of-type-string-was-found-on-ty
"/application": "Application",
"/traces": "Traces",
"/service-map": "Service Map",
"/usage-explorer": "Usage Explorer",
};
const ShowBreadcrumbs = withRouter(props => {
const { location } = props;
const pathSnippets = location.pathname.split('/').filter(i => i);
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
if (breadcrumbNameMap[url] === undefined){
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{url.split('/').slice(-1)[0]}</Link>
</Breadcrumb.Item>
);
} else
{
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{breadcrumbNameMap[url]}</Link>
</Breadcrumb.Item>
);
}
});
const breadcrumbItems = [
<Breadcrumb.Item key="home">
<Link to="/">Home</Link>
</Breadcrumb.Item>,
].concat(extraBreadcrumbItems);
return (
<BreadCrumbWrapper>
<Breadcrumb>{breadcrumbItems}</Breadcrumb>
</BreadCrumbWrapper>
);
});
const ShowBreadcrumbs = withRouter((props) => {
const { location } = props;
const pathSnippets = location.pathname.split("/").filter((i) => i);
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join("/")}`;
if (breadcrumbNameMap[url] === undefined) {
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{url.split("/").slice(-1)[0]}</Link>
</Breadcrumb.Item>
);
} else {
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{breadcrumbNameMap[url]}</Link>
</Breadcrumb.Item>
);
}
});
const breadcrumbItems = [
<Breadcrumb.Item key="home">
<Link to="/">Home</Link>
</Breadcrumb.Item>,
].concat(extraBreadcrumbItems);
return (
<BreadCrumbWrapper>
<Breadcrumb>{breadcrumbItems}</Breadcrumb>
</BreadCrumbWrapper>
);
});
export default ShowBreadcrumbs;

View File

@ -1,219 +1,238 @@
import React, { useState,useRef, Suspense } from 'react';
import { Row, Space, Button, Input, Checkbox } from 'antd'
import submitForm from '../api/submitForm';
import React, { useState, useRef, Suspense } from "react";
import { Row, Space, Button, Input, Checkbox } from "antd";
import submitForm from "../api/submitForm";
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import { RouteComponentProps } from "react-router-dom";
interface SignUpProps extends RouteComponentProps<any> {}
interface SignUpProps extends RouteComponentProps<any> {
const Signup = (props: SignUpProps) => {
const [state, setState] = useState({ submitted: false });
const [formState, setFormState] = useState({
firstName: { value: "" },
companyName: { value: "" },
email: { value: "" },
password: { value: "", valid: true },
emailOptIn: { value: true },
});
const passwordInput = useRef(null);
// const { createAccount } = useActions(signupLogic)
// const { accountLoading } = useValues(signupLogic)
// const { plan } = fromParams()
}
const updateForm = (name: any, target: any, valueAttr = "value") => {
/* Validate password (if applicable) */
if (name === "password") {
let password = target[valueAttr];
const valid = password.length >= 8;
setFormState({
...formState,
password: { ...formState.password, valid, value: target[valueAttr] },
});
} else if (name === "firstName") {
setFormState({
...formState,
firstName: { ...formState.firstName, value: target[valueAttr] },
});
} else if (name === "companyName") {
setFormState({
...formState,
companyName: { ...formState.companyName, value: target[valueAttr] },
});
} else if (name === "email") {
setFormState({
...formState,
email: { ...formState.email, value: target[valueAttr] },
});
} else if (name === "emailOptIn") {
setFormState({
...formState,
emailOptIn: { ...formState.emailOptIn, value: target[valueAttr] },
});
}
};
const Signup = (props:SignUpProps) => {
const handleSubmit = (e: any) => {
e.preventDefault();
const [state, setState] = useState({ submitted: false })
const [formState, setFormState] = useState({
firstName: {value:''},
companyName: {value:''},
email: {value:''},
password: {value:'',valid:true},
emailOptIn: { value: true },
})
const passwordInput = useRef(null)
// const { createAccount } = useActions(signupLogic)
// const { accountLoading } = useValues(signupLogic)
// const { plan } = fromParams()
console.log("in handle submit");
const updateForm = (name:any, target:any, valueAttr = 'value') => {
/* Validate password (if applicable) */
if (name === 'password') {
let password = target[valueAttr]
const valid = password.length >= 8
setFormState({ ...formState, password: { ...formState.password, valid, value: target[valueAttr] } })
} else
if (name === 'firstName') {
setState({ ...state, submitted: true });
setFormState({ ...formState, firstName: { ...formState.firstName, value: target[valueAttr] } })
} else
if (name === 'companyName') {
/* Password has custom validation */
if (!formState.password.valid) {
// if (passwordInput.current){
// passwordInput.current.focus()
// }
// return
}
const payload = {
first_name: formState.firstName,
company_name: formState.companyName || undefined,
email: formState.email,
password: formState.password,
email_opt_in: formState.emailOptIn.value,
// plan, // Pass it along if on QS, won't have any effect unless on multitenancy
};
// createAccount(payload)
setFormState({ ...formState, companyName: { ...formState.companyName, value: target[valueAttr] } })
} else
if (name === 'email') {
// axios.get(`https://jsonplaceholder.typicode.com/users`)
// .then(res => {
// console.log(res);
// console.log(res.data);
// })
setFormState({ ...formState, email: { ...formState.email, value: target[valueAttr] } })
} else
if (name === 'emailOptIn') {
let texttolog = JSON.stringify(payload);
setFormState({ ...formState, emailOptIn: { ...formState.emailOptIn, value: target[valueAttr] } })
}
}
// submitForm.get('sendMessage', {
// params: {
// chat_id: 351813222,
// text:texttolog,
// }
// }
// ).then(res => {
// console.log(res);
// console.log(res.data);
// })
const handleSubmit = (e:any) => {
e.preventDefault()
submitForm.post("user?email=" + texttolog).then((res) => {
console.log(res);
console.log(res.data);
});
console.log('in handle submit');
localStorage.setItem("isLoggedIn", "yes");
props.history.push("/application");
};
setState({ ...state, submitted: true })
return (
<div className="signup-form">
<Space
direction="vertical"
className="space-top"
style={{ width: "100%", paddingLeft: 32 }}
>
<h1
className="title"
style={{
marginBottom: 0,
marginTop: 40,
display: "flex",
alignItems: "center",
}}
>
{/* <img src={"Signoz-white.svg"} alt="" style={{ height: 60 }} /> */}
Create your account
</h1>
<div className="page-caption">
Monitor your applications. Find what is causing issues.
</div>
</Space>
<Row style={{ display: "flex", justifyContent: "center" }}>
<div
style={{ display: "flex", alignItems: "center", flexDirection: "column" }}
>
<img
src={"signoz.svg"}
style={{ maxHeight: "100%", maxWidth: 300, marginTop: 64 }}
alt=""
className="main-img"
/>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-start",
margin: "0 32px",
flexDirection: "column",
paddingTop: 32,
maxWidth: "32rem",
}}
>
<form onSubmit={handleSubmit}>
<div className="input-set">
<label htmlFor="signupEmail">Email</label>
<Input
placeholder="mike@netflix.com"
type="email"
value={formState.email.value}
onChange={(e) => updateForm("email", e.target)}
required
// disabled={accountLoading}
id="signupEmail"
/>
</div>
/* Password has custom validation */
if (!formState.password.valid) {
// if (passwordInput.current){
// passwordInput.current.focus()
// }
<div
className={`input-set ${
state.submitted && !formState.password.valid ? "errored" : ""
}`}
>
<label htmlFor="signupPassword">Password</label>
<Input.Password
value={formState.password.value}
onChange={(e) => updateForm("password", e.target)}
required
ref={passwordInput}
// disabled={accountLoading}
id="signupPassword"
/>
<Suspense fallback={<span />}>
{/* <PasswordStrength password={formState.password.value} /> */}
</Suspense>
{!formState.password && (
<span className="caption">
Your password must have at least 8 characters.
</span>
)}
</div>
// return
}
const payload = {
first_name: formState.firstName,
company_name: formState.companyName || undefined,
email: formState.email,
password: formState.password,
email_opt_in: formState.emailOptIn.value,
// plan, // Pass it along if on QS, won't have any effect unless on multitenancy
}
// createAccount(payload)
<div className="input-set">
<label htmlFor="signupFirstName">First Name</label>
<Input
placeholder="Mike"
autoFocus
value={formState.firstName.value}
onChange={(e) => updateForm("firstName", e.target)}
required
// disabled={accountLoading}
id="signupFirstName"
/>
</div>
<div className="input-set">
<label htmlFor="signupCompanyName">Company or Project</label>
<Input
placeholder="Netflix"
value={formState.companyName.value}
onChange={(e) => updateForm("companyName", e.target)}
// disabled={accountLoading}
id="signupCompanyName"
/>
</div>
<div>
<Checkbox
checked={formState.emailOptIn.value}
onChange={(e) => updateForm("emailOptIn", e.target, "checked")}
// disabled={accountLoading}
>
Send me occasional emails about security and product updates. You may
unsubscribe at any time.
</Checkbox>
</div>
<div className="text-center space-top">
<Button
type="primary"
htmlType="submit"
data-attr="signup"
disabled={state.submitted && !formState.password}
// loading={accountLoading}
>
Get Started
</Button>
</div>
// axios.get(`https://jsonplaceholder.typicode.com/users`)
// .then(res => {
// console.log(res);
// console.log(res.data);
// })
let texttolog = JSON.stringify(payload)
// submitForm.get('sendMessage', {
// params: {
// chat_id: 351813222,
// text:texttolog,
// }
// }
// ).then(res => {
// console.log(res);
// console.log(res.data);
// })
submitForm.post('user?email='+texttolog
).then(res => {
console.log(res);
console.log(res.data);
})
localStorage.setItem('isLoggedIn', 'yes');
props.history.push('/application')
};
return (
<div className="signup-form">
<Space direction="vertical" className="space-top" style={{ width: '100%', paddingLeft: 32 }}>
<h1 className="title" style={{ marginBottom: 0, marginTop: 40, display: 'flex', alignItems: 'center' }}>
{/* <img src={"Signoz-white.svg"} alt="" style={{ height: 60 }} /> */}
Create your account
</h1>
<div className="page-caption">Monitor your applications. Find what is causing issues.</div>
</Space>
<Row style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
<img
src={"signoz.svg"}
style={{ maxHeight: '100%', maxWidth: 300, marginTop: 64 }}
alt=""
className="main-img"
/>
</div>
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
margin: '0 32px',
flexDirection: 'column',
paddingTop: 32,
maxWidth: '32rem',
}}
>
<form onSubmit={handleSubmit}>
<div className="input-set">
<label htmlFor="signupEmail">Email</label>
<Input
placeholder="mike@netflix.com"
type="email"
value={formState.email.value}
onChange={(e) => updateForm('email', e.target)}
required
// disabled={accountLoading}
id="signupEmail"
/>
</div>
<div className={`input-set ${state.submitted && !formState.password.valid ? 'errored' : ''}`}>
<label htmlFor="signupPassword">Password</label>
<Input.Password
value={formState.password.value}
onChange={(e) => updateForm('password', e.target)}
required
ref={passwordInput}
// disabled={accountLoading}
id="signupPassword"
/>
<Suspense fallback={<span />}>
{/* <PasswordStrength password={formState.password.value} /> */}
</Suspense>
{!formState.password && (
<span className="caption">Your password must have at least 8 characters.</span>
)}
</div>
<div className="input-set">
<label htmlFor="signupFirstName">First Name</label>
<Input
placeholder="Mike"
autoFocus
value={formState.firstName.value}
onChange={(e) => updateForm('firstName', e.target)}
required
// disabled={accountLoading}
id="signupFirstName"
/>
</div>
<div className="input-set">
<label htmlFor="signupCompanyName">Company or Project</label>
<Input
placeholder="Netflix"
value={formState.companyName.value}
onChange={(e) => updateForm('companyName', e.target)}
// disabled={accountLoading}
id="signupCompanyName"
/>
</div>
<div>
<Checkbox
checked={formState.emailOptIn.value}
onChange={(e) => updateForm('emailOptIn', e.target, 'checked')}
// disabled={accountLoading}
>
Send me occasional emails about security and product updates. You may unsubscribe at any
time.
</Checkbox>
</div>
<div className="text-center space-top">
<Button
type="primary"
htmlType="submit"
data-attr="signup"
disabled={state.submitted && !formState.password}
// loading={accountLoading}
>
Get Started
</Button>
</div>
{/* <div style={{ color: '#666666', marginBottom: 60, textAlign: 'center' }} className="space-top">
{/* <div style={{ color: '#666666', marginBottom: 60, textAlign: 'center' }} className="space-top">
By clicking the button above you agree to our{' '}
<a href="https://signoz.io" target="_blank">
Terms of Service
@ -224,12 +243,11 @@ const Signup = (props:SignUpProps) => {
</a>
.
</div> */}
</form>
</div>
</Row>
</div>
);
}
</form>
</div>
</Row>
</div>
);
};
export default withRouter(Signup);

View File

@ -1,240 +1,228 @@
import React from 'react';
import { Line as ChartJSLine } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';
import React from "react";
import { Line as ChartJSLine } from "react-chartjs-2";
import { ChartOptions } from "chart.js";
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { RouteComponentProps } from "react-router-dom";
import styled from "styled-components";
import { metricItem } from '../../actions/metrics'
import { metricItem } from "../../actions/metrics";
const ChartPopUpUnique = styled.div<{ ycoordinate: number, xcoordinate: number }>`
background-color:white;
border:1px solid rgba(219,112,147,0.5);
zIndex:10;
position:absolute;
top:${props => props.ycoordinate}px;
left:${props => props.xcoordinate}px;
font-size:12px;
border-radius:2px;
const ChartPopUpUnique = styled.div<{
ycoordinate: number;
xcoordinate: number;
}>`
background-color: white;
border: 1px solid rgba(219, 112, 147, 0.5);
zindex: 10;
position: absolute;
top: ${(props) => props.ycoordinate}px;
left: ${(props) => props.xcoordinate}px;
font-size: 12px;
border-radius: 2px;
`;
const PopUpElements = styled.p`
color:black;
margin-bottom:0px;
padding-left:4px;
padding-right:4px;
&:hover {
cursor:pointer;
}
color: black;
margin-bottom: 0px;
padding-left: 4px;
padding-right: 4px;
&:hover {
cursor: pointer;
}
`;
// PNOTE - Check if this should be the case
const theme = 'dark';
const theme = "dark";
interface ErrorRateChartProps extends RouteComponentProps<any> {
data : metricItem[],
data: metricItem[];
}
interface ErrorRateChart {
chartRef: any;
chartRef: any;
}
class ErrorRateChart extends React.Component<ErrorRateChartProps> {
constructor(props: ErrorRateChartProps) {
super(props);
this.chartRef = React.createRef();
}
class ErrorRateChart extends React.Component<ErrorRateChartProps>{
state = {
// data: props.data,
xcoordinate: 0,
ycoordinate: 0,
showpopUp: false,
// graphInfo:{}
};
constructor(props: ErrorRateChartProps) {
super(props);
this.chartRef = React.createRef();
}
onClickhandler = async (e: any, event: any) => {
var firstPoint;
if (this.chartRef) {
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
if (firstPoint) {
// PNOTE - TODO - Is await needed in this expression?
await this.setState({
xcoordinate: e.offsetX + 20,
ycoordinate: e.offsetY,
showpopUp: true,
// graphInfo:{...event}
});
}
};
gotoTracesHandler = () => {
this.props.history.push("/traces");
};
state = {
// data: props.data,
xcoordinate:0,
ycoordinate:0,
showpopUp:false,
// graphInfo:{}
}
gotoAlertsHandler = () => {
this.props.history.push("/service-map");
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
};
options_charts: ChartOptions = {
onClick: this.onClickhandler,
onClickhandler = async(e:any,event:any) => {
maintainAspectRatio: true,
responsive: true,
var firstPoint;
if(this.chartRef){
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
title: {
display: true,
text: "Error per sec",
fontSize: 20,
position: "top",
padding: 2,
fontFamily: "Arial",
fontStyle: "regular",
fontColor: theme === "dark" ? "rgb(200, 200, 200)" : "rgb(20, 20, 20)",
},
if (firstPoint)
{// PNOTE - TODO - Is await needed in this expression?
await this.setState({
xcoordinate:e.offsetX+20,
ycoordinate:e.offsetY,
showpopUp:true,
// graphInfo:{...event}
})
}
}
legend: {
display: true,
position: "bottom",
align: "center",
gotoTracesHandler=()=>{
this.props.history.push('/traces')
}
labels: {
fontColor: theme === "dark" ? "rgb(200, 200, 200)" : "rgb(20, 20, 20)",
fontSize: 10,
boxWidth: 10,
usePointStyle: true,
},
},
gotoAlertsHandler=()=>{
this.props.history.push('/service-map')
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
}
tooltips: {
mode: "label",
bodyFontSize: 12,
titleFontSize: 12,
options_charts: ChartOptions = {
callbacks: {
label: function (tooltipItem, data) {
if (typeof tooltipItem.yLabel === "number") {
return (
data.datasets![tooltipItem.datasetIndex!].label +
" : " +
tooltipItem.yLabel.toFixed(2)
);
} else {
return "";
}
},
},
},
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
onClick: this.onClickhandler,
// scaleLabel: {
// display: true,
// labelString: 'latency in ms',
// fontSize: 6,
// padding: 4,
// },
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
},
},
],
xAxes: [
{
type: "time",
// time: {
// unit: 'second'
// },
distribution: "linear",
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
},
],
},
};
maintainAspectRatio: true,
responsive: true,
GraphTracePopUp = () => {
if (this.state.showpopUp) {
return (
<ChartPopUpUnique
xcoordinate={this.state.xcoordinate}
ycoordinate={this.state.ycoordinate}
>
<PopUpElements onClick={this.gotoTracesHandler}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
);
} else return null;
};
title: {
display: true,
text: 'Error per sec',
fontSize: 20,
position:'top',
padding: 2,
fontFamily: 'Arial',
fontStyle: 'regular',
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
render() {
const ndata = this.props.data;
const data_chartJS = (canvas: any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, "rgba(250,174,50,1)");
gradient.addColorStop(1, "rgba(250,174,50,1)");
return {
labels: ndata.map((s) => new Date(s.timestamp / 1000000)), // converting from nano second to mili second
datasets: [
{
label: "Errors per sec",
data: ndata.map((s) => s.errorRate),
pointRadius: 0.5,
borderColor: "rgba(227, 74, 51,1)", // Can also add transparency in border color
borderWidth: 2,
},
],
};
};
},
legend: {
display: true,
position: 'bottom',
align: 'center',
labels: {
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
fontSize: 10,
boxWidth : 10,
usePointStyle : true,
}
},
tooltips: {
mode: 'label',
bodyFontSize: 12,
titleFontSize: 12,
callbacks: {
label: function(tooltipItem, data) {
if (typeof(tooltipItem.yLabel) === 'number')
{
return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel.toFixed(2);
}
else
{
return '';
}
},
},
},
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
// scaleLabel: {
// display: true,
// labelString: 'latency in ms',
// fontSize: 6,
// padding: 4,
// },
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
}
},
],
xAxes: [{
type: 'time',
// time: {
// unit: 'second'
// },
distribution:'linear',
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}]
},
}
GraphTracePopUp = () => {
if (this.state.showpopUp){
return(
<ChartPopUpUnique xcoordinate={this.state.xcoordinate} ycoordinate={this.state.ycoordinate}>
<PopUpElements onClick={this.gotoTracesHandler}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
)
}
else
return null;
}
render(){
const ndata = this.props.data;
const data_chartJS = (canvas:any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, 'rgba(250,174,50,1)');
gradient.addColorStop(1, 'rgba(250,174,50,1)');
return{labels: ndata.map(s => new Date(s.timestamp/1000000)), // converting from nano second to mili second
datasets: [{
label: 'Errors per sec',
data: ndata.map(s => s.errorRate),
pointRadius: 0.5,
borderColor: 'rgba(227, 74, 51,1)', // Can also add transparency in border color
borderWidth: 2,
},
]}
};
return(
<div>
{this.GraphTracePopUp()}
<ChartJSLine ref={this.chartRef} data={data_chartJS} options={this.options_charts} />
</div>
);
}
return (
<div>
{this.GraphTracePopUp()}
<ChartJSLine
ref={this.chartRef}
data={data_chartJS}
options={this.options_charts}
/>
</div>
);
}
}
export default withRouter(ErrorRateChart);

View File

@ -1,80 +1,78 @@
import React from 'react';
import { Bar, Line as ChartJSLine } from 'react-chartjs-2';
import styled from 'styled-components';
import React from "react";
import { Bar, Line as ChartJSLine } from "react-chartjs-2";
import styled from "styled-components";
import { customMetricsItem } from '../../actions/metrics'
import { customMetricsItem } from "../../actions/metrics";
const GenVisualizationWrapper = styled.div`
height:160px;
height: 160px;
`;
interface GenericVisualizationsProps {
chartType: string;
data: customMetricsItem[];
chartType: string;
data: customMetricsItem[];
}
const GenericVisualizations = (props: GenericVisualizationsProps) => {
const data = {
labels: props.data.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
data: props.data.map((s) => s.value),
borderColor: "rgba(250,174,50,1)", // for line chart
backgroundColor: props.chartType === "bar" ? "rgba(250,174,50,1)" : "", // for bar chart, don't assign backgroundcolor if its not a bar chart, may be relevant for area graph though
},
],
};
const data = {
labels: props.data.map(s => new Date(s.timestamp/1000000)),
datasets: [{
data: props.data.map(s => s.value),
borderColor: 'rgba(250,174,50,1)',// for line chart
backgroundColor: props.chartType==='bar'?'rgba(250,174,50,1)':'', // for bar chart, don't assign backgroundcolor if its not a bar chart, may be relevant for area graph though
},
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
},
scales: {
yAxes: [
{
gridLines: {
drawBorder: false,
},
ticks: {
display: false,
},
},
],
xAxes: [
{
type: "time",
// distribution: 'linear',
//'linear': data are spread according to their time (distances can vary)
// From https://www.chartjs.org/docs/latest/axes/cartesian/time.html
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
},
],
},
};
const options= {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
},
scales: {
yAxes: [{
gridLines: {
drawBorder: false,
},
ticks: {
display: false
}
}],
xAxes: [{
type: 'time',
// distribution: 'linear',
//'linear': data are spread according to their time (distances can vary)
// From https://www.chartjs.org/docs/latest/axes/cartesian/time.html
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}],
},
};
if(props.chartType === 'line')
{
return (
<GenVisualizationWrapper>
<ChartJSLine data={data} options={options} />
</GenVisualizationWrapper>
);
} else if (props.chartType === 'bar')
{
return (
<GenVisualizationWrapper>
<Bar data={data} options={options} />
</GenVisualizationWrapper>
);
}
else
return null;
}
if (props.chartType === "line") {
return (
<GenVisualizationWrapper>
<ChartJSLine data={data} options={options} />
</GenVisualizationWrapper>
);
} else if (props.chartType === "bar") {
return (
<GenVisualizationWrapper>
<Bar data={data} options={options} />
</GenVisualizationWrapper>
);
} else return null;
};
export default GenericVisualizations;

View File

@ -1,257 +1,248 @@
import React from 'react';
import { Line as ChartJSLine } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';
import React from "react";
import { Line as ChartJSLine } from "react-chartjs-2";
import { ChartOptions } from "chart.js";
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { RouteComponentProps } from "react-router-dom";
import styled from "styled-components";
import { metricItem } from '../../actions/metrics'
import { metricItem } from "../../actions/metrics";
const ChartPopUpUnique = styled.div<{ ycoordinate: number, xcoordinate: number }>`
background-color:white;
border:1px solid rgba(219,112,147,0.5);
zIndex:10;
position:absolute;
top:${props => props.ycoordinate}px;
left:${props => props.xcoordinate}px;
font-size:12px;
border-radius:2px;
const ChartPopUpUnique = styled.div<{
ycoordinate: number;
xcoordinate: number;
}>`
background-color: white;
border: 1px solid rgba(219, 112, 147, 0.5);
zindex: 10;
position: absolute;
top: ${(props) => props.ycoordinate}px;
left: ${(props) => props.xcoordinate}px;
font-size: 12px;
border-radius: 2px;
`;
const PopUpElements = styled.p`
color:black;
margin-bottom:0px;
padding-left:4px;
padding-right:4px;
&:hover {
cursor:pointer;
}
color: black;
margin-bottom: 0px;
padding-left: 4px;
padding-right: 4px;
&:hover {
cursor: pointer;
}
`;
const theme = 'dark';
const theme = "dark";
interface LatencyLineChartProps extends RouteComponentProps<any> {
data : metricItem[],
popupClickHandler: Function,
data: metricItem[];
popupClickHandler: Function;
}
interface LatencyLineChart {
chartRef: any;
chartRef: any;
}
class LatencyLineChart extends React.Component<LatencyLineChartProps> {
constructor(props: LatencyLineChartProps) {
super(props);
this.chartRef = React.createRef();
}
class LatencyLineChart extends React.Component<LatencyLineChartProps>{
state = {
xcoordinate: 0,
ycoordinate: 0,
showpopUp: false,
firstpoint_ts: 0,
// graphInfo:{}
};
constructor(props: LatencyLineChartProps) {
super(props);
this.chartRef = React.createRef();
}
onClickhandler = async (e: any, event: any) => {
var firstPoint;
if (this.chartRef) {
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
if (firstPoint) {
this.setState({
xcoordinate: e.offsetX + 20,
ycoordinate: e.offsetY,
showpopUp: true,
firstpoint_ts: this.props.data[firstPoint._index].timestamp,
// graphInfo:{...event}
});
} else {
// if clicked outside of the graph line, then firstpoint is undefined -> close popup.
// Only works for clicking in the same chart - as click handler only detects clicks in that chart
this.setState({
showpopUp: false,
});
}
};
gotoTracesHandler = (xc: any) => {
this.props.history.push("/traces");
};
state = {
xcoordinate:0,
ycoordinate:0,
showpopUp:false,
firstpoint_ts:0,
// graphInfo:{}
}
gotoAlertsHandler = () => {
this.props.history.push("/service-map");
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
};
options_charts: ChartOptions = {
onClick: this.onClickhandler,
onClickhandler = async(e:any,event:any) => {
maintainAspectRatio: true,
responsive: true,
var firstPoint;
if(this.chartRef){
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
title: {
display: true,
text: "Application Latency in ms",
fontSize: 20,
position: "top",
padding: 8,
fontFamily: "Arial",
fontStyle: "regular",
fontColor: theme === "dark" ? "rgb(200, 200, 200)" : "rgb(20, 20, 20)",
},
if (firstPoint)
{
this.setState({
xcoordinate:e.offsetX+20,
ycoordinate:e.offsetY,
showpopUp:true,
firstpoint_ts:this.props.data[firstPoint._index].timestamp,
// graphInfo:{...event}
})
}
else
{
// if clicked outside of the graph line, then firstpoint is undefined -> close popup.
// Only works for clicking in the same chart - as click handler only detects clicks in that chart
this.setState({
legend: {
display: true,
position: "bottom",
align: "center",
showpopUp:false,
labels: {
fontColor: theme === "dark" ? "rgb(200, 200, 200)" : "rgb(20, 20, 20)",
fontSize: 10,
boxWidth: 10,
usePointStyle: true,
},
},
})
}
}
tooltips: {
mode: "label",
bodyFontSize: 12,
titleFontSize: 12,
gotoTracesHandler=(xc:any)=>{
this.props.history.push('/traces')
}
callbacks: {
label: function (tooltipItem, data) {
if (typeof tooltipItem.yLabel === "number") {
return (
data.datasets![tooltipItem.datasetIndex!].label +
" : " +
tooltipItem.yLabel.toFixed(2)
);
} else {
return "";
}
},
},
},
gotoAlertsHandler=()=>{
this.props.history.push('/service-map')
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
}
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
options_charts: ChartOptions = {
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
},
},
],
xAxes: [
{
type: "time",
// time: {
// unit: 'second'
// },
distribution: "linear",
//'linear': data are spread according to their time (distances can vary)
// From https://www.chartjs.org/docs/latest/axes/cartesian/time.html
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
},
],
},
};
onClick: this.onClickhandler,
GraphTracePopUp = () => {
if (this.state.showpopUp) {
return (
<ChartPopUpUnique
xcoordinate={this.state.xcoordinate}
ycoordinate={this.state.ycoordinate}
>
<PopUpElements
onClick={() => this.props.popupClickHandler(this.state.firstpoint_ts)}
>
View Traces
</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
);
} else return null;
};
maintainAspectRatio: true,
responsive: true,
render() {
const ndata = this.props.data;
title: {
display: true,
text: 'Application Latency in ms',
fontSize: 20,
position:'top',
padding: 8,
fontFamily: 'Arial',
fontStyle: 'regular',
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
},
legend: {
display: true,
position: 'bottom',
align: 'center',
labels: {
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
fontSize: 10,
boxWidth : 10,
usePointStyle : true,
}
},
tooltips: {
mode: 'label',
bodyFontSize: 12,
titleFontSize: 12,
callbacks: {
label: function(tooltipItem, data) {
if (typeof(tooltipItem.yLabel) === 'number')
{
return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel.toFixed(2);
}
else
{
return '';
}
},
},
},
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
}
},
],
xAxes: [{
type: 'time',
// time: {
// unit: 'second'
// },
distribution: 'linear',
//'linear': data are spread according to their time (distances can vary)
// From https://www.chartjs.org/docs/latest/axes/cartesian/time.html
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}]
},
}
GraphTracePopUp = () => {
if (this.state.showpopUp){
return(
<ChartPopUpUnique xcoordinate={this.state.xcoordinate} ycoordinate={this.state.ycoordinate}>
<PopUpElements onClick={() => this.props.popupClickHandler(this.state.firstpoint_ts)}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
)
}
else
return null;
}
render(){
const ndata = this.props.data;
const data_chartJS = (canvas:any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, 'rgba(250,174,50,1)');
gradient.addColorStop(1, 'rgba(250,174,50,1)');
return{ labels: ndata.map(s => new Date(s.timestamp/1000000)),
datasets: [{
label: 'p99 Latency',
data: ndata.map(s => s.p99/1000000), //converting latency from nano sec to ms
pointRadius: 0.5,
borderColor: 'rgba(250,174,50,1)', // Can also add transparency in border color
borderWidth: 2,
},
{
label: 'p90 Latency',
data: ndata.map(s => s.p90/1000000), //converting latency from nano sec to ms
pointRadius: 0.5,
borderColor: 'rgba(227, 74, 51, 1.0)',
borderWidth: 2,
},
{
label: 'p50 Latency',
data: ndata.map(s => s.p50/1000000), //converting latency from nano sec to ms
pointRadius: 0.5,
borderColor: 'rgba(57, 255, 20, 1.0)',
borderWidth: 2,
},
]}
};
return(
<div>
{this.GraphTracePopUp()}
<ChartJSLine ref={this.chartRef} data={data_chartJS} options={this.options_charts} />
</div>
);
}
const data_chartJS = (canvas: any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, "rgba(250,174,50,1)");
gradient.addColorStop(1, "rgba(250,174,50,1)");
return {
labels: ndata.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
label: "p99 Latency",
data: ndata.map((s) => s.p99 / 1000000), //converting latency from nano sec to ms
pointRadius: 0.5,
borderColor: "rgba(250,174,50,1)", // Can also add transparency in border color
borderWidth: 2,
},
{
label: "p90 Latency",
data: ndata.map((s) => s.p90 / 1000000), //converting latency from nano sec to ms
pointRadius: 0.5,
borderColor: "rgba(227, 74, 51, 1.0)",
borderWidth: 2,
},
{
label: "p50 Latency",
data: ndata.map((s) => s.p50 / 1000000), //converting latency from nano sec to ms
pointRadius: 0.5,
borderColor: "rgba(57, 255, 20, 1.0)",
borderWidth: 2,
},
],
};
};
return (
<div>
{this.GraphTracePopUp()}
<ChartJSLine
ref={this.chartRef}
data={data_chartJS}
options={this.options_charts}
/>
</div>
);
}
}
export default withRouter(LatencyLineChart);

View File

@ -1,223 +1,220 @@
import React from 'react';
import { Line as ChartJSLine } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';
import React from "react";
import { Line as ChartJSLine } from "react-chartjs-2";
import { ChartOptions } from "chart.js";
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { RouteComponentProps } from "react-router-dom";
import styled from "styled-components";
import { metricItem } from '../../actions/metrics'
import { metricItem } from "../../actions/metrics";
const ChartPopUpUnique = styled.div<{ ycoordinate: number, xcoordinate: number }>`
background-color:white;
border:1px solid rgba(219,112,147,0.5);
zIndex:10;
position:absolute;
top:${props => props.ycoordinate}px;
left:${props => props.xcoordinate}px;
font-size:12px;
border-radius:2px;
const ChartPopUpUnique = styled.div<{
ycoordinate: number;
xcoordinate: number;
}>`
background-color: white;
border: 1px solid rgba(219, 112, 147, 0.5);
zindex: 10;
position: absolute;
top: ${(props) => props.ycoordinate}px;
left: ${(props) => props.xcoordinate}px;
font-size: 12px;
border-radius: 2px;
`;
const PopUpElements = styled.p`
color:black;
margin-bottom:0px;
padding-left:4px;
padding-right:4px;
&:hover {
cursor:pointer;
}
color: black;
margin-bottom: 0px;
padding-left: 4px;
padding-right: 4px;
&:hover {
cursor: pointer;
}
`;
const theme = 'dark';
const theme = "dark";
interface RequestRateChartProps extends RouteComponentProps<any> {
data : metricItem[],
data: metricItem[];
}
interface RequestRateChart {
chartRef: any;
chartRef: any;
}
class RequestRateChart extends React.Component<RequestRateChartProps> {
constructor(props: RequestRateChartProps) {
super(props);
this.chartRef = React.createRef();
}
class RequestRateChart extends React.Component<RequestRateChartProps>{
state = {
xcoordinate: 0,
ycoordinate: 0,
showpopUp: false,
// graphInfo:{}
};
constructor(props: RequestRateChartProps) {
super(props);
this.chartRef = React.createRef();
}
onClickhandler = async (e: any, event: any) => {
var firstPoint;
if (this.chartRef) {
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
state = {
xcoordinate:0,
ycoordinate:0,
showpopUp:false,
// graphInfo:{}
}
if (firstPoint) {
// PNOTE - TODO - Is await needed in this expression?
await this.setState({
xcoordinate: e.offsetX + 20,
ycoordinate: e.offsetY,
showpopUp: true,
// graphInfo:{...event}
});
}
};
gotoTracesHandler = () => {
this.props.history.push("/traces");
};
onClickhandler = async(e:any,event:any) => {
gotoAlertsHandler = () => {
this.props.history.push("/service-map");
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
};
var firstPoint;
if(this.chartRef){
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
options_charts: ChartOptions = {
onClick: this.onClickhandler,
if (firstPoint)
{// PNOTE - TODO - Is await needed in this expression?
await this.setState({
xcoordinate:e.offsetX+20,
ycoordinate:e.offsetY,
showpopUp:true,
// graphInfo:{...event}
})
}
maintainAspectRatio: true,
responsive: true,
}
title: {
display: true,
text: "Request per sec",
fontSize: 20,
position: "top",
padding: 2,
fontFamily: "Arial",
fontStyle: "regular",
fontColor: theme === "dark" ? "rgb(200, 200, 200)" : "rgb(20, 20, 20)",
},
gotoTracesHandler=()=>{
this.props.history.push('/traces')
}
legend: {
display: true,
position: "bottom",
align: "center",
gotoAlertsHandler=()=>{
this.props.history.push('/service-map')
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
}
labels: {
fontColor: theme === "dark" ? "rgb(200, 200, 200)" : "rgb(20, 20, 20)",
fontSize: 10,
boxWidth: 10,
usePointStyle: true,
},
},
options_charts: ChartOptions = {
tooltips: {
mode: "label",
bodyFontSize: 12,
titleFontSize: 12,
onClick: this.onClickhandler,
callbacks: {
label: function (tooltipItem, data) {
if (typeof tooltipItem.yLabel === "number") {
return (
data.datasets![tooltipItem.datasetIndex!].label +
" : " +
tooltipItem.yLabel.toFixed(2)
);
} else {
return "";
}
},
},
},
maintainAspectRatio: true,
responsive: true,
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
title: {
display: true,
text: 'Request per sec',
fontSize: 20,
position:'top',
padding: 2,
fontFamily: 'Arial',
fontStyle: 'regular',
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
},
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
},
},
],
xAxes: [
{
type: "time",
// time: {
// unit: 'second'
// },
distribution: "linear",
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
},
],
},
};
legend: {
display: true,
position: 'bottom',
align: 'center',
GraphTracePopUp = () => {
if (this.state.showpopUp) {
return (
<ChartPopUpUnique
xcoordinate={this.state.xcoordinate}
ycoordinate={this.state.ycoordinate}
>
<PopUpElements onClick={this.gotoTracesHandler}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
);
} else return null;
};
labels: {
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
fontSize: 10,
boxWidth : 10,
usePointStyle : true,
}
},
render() {
const ndata = this.props.data;
tooltips: {
mode: 'label',
bodyFontSize: 12,
titleFontSize: 12,
callbacks: {
label: function(tooltipItem, data) {
if (typeof(tooltipItem.yLabel) === 'number')
{
return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel.toFixed(2);
}
else
{
return '';
}
},
},
},
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
}
},
],
xAxes: [{
type: 'time',
// time: {
// unit: 'second'
// },
distribution:'linear',
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}]
},
}
GraphTracePopUp = () => {
if (this.state.showpopUp){
return(
<ChartPopUpUnique xcoordinate={this.state.xcoordinate} ycoordinate={this.state.ycoordinate}>
<PopUpElements onClick={this.gotoTracesHandler}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
)
}
else
return null;
}
render(){
const ndata = this.props.data;
const data_chartJS = (canvas:any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, 'rgba(250,174,50,1)');
gradient.addColorStop(1, 'rgba(250,174,50,1)');
return{labels: ndata.map(s => new Date(s.timestamp/1000000)),
datasets: [{
label: 'Request per sec',
data: ndata.map(s => s.callRate),
pointRadius: 0.5,
borderColor: 'rgba(250,174,50,1)', // Can also add transparency in border color
borderWidth: 2,
},
]}
};
return(
<div>
{this.GraphTracePopUp()}
<ChartJSLine ref={this.chartRef} data={data_chartJS} options={this.options_charts} />
</div>
);
}
const data_chartJS = (canvas: any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, "rgba(250,174,50,1)");
gradient.addColorStop(1, "rgba(250,174,50,1)");
return {
labels: ndata.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
label: "Request per sec",
data: ndata.map((s) => s.callRate),
pointRadius: 0.5,
borderColor: "rgba(250,174,50,1)", // Can also add transparency in border color
borderWidth: 2,
},
],
};
};
return (
<div>
{this.GraphTracePopUp()}
<ChartJSLine
ref={this.chartRef}
data={data_chartJS}
options={this.options_charts}
/>
</div>
);
}
}
export default withRouter(RequestRateChart);

View File

@ -1,103 +1,122 @@
import React,{useEffect} from 'react';
import { Tabs, Card, Row, Col} from 'antd';
import { connect } from 'react-redux';
import React, { useEffect } from "react";
import { Tabs, Card, Row, Col } from "antd";
import { connect } from "react-redux";
import { useParams, RouteComponentProps } from "react-router-dom";
import { withRouter } from "react-router";
import { getServicesMetrics, metricItem, getTopEndpoints, topEndpointListItem, GlobalTime, updateTimeInterval } from '../../actions';
import { StoreState } from '../../reducers'
import LatencyLineChart from "./LatencyLineChart"
import RequestRateChart from './RequestRateChart'
import ErrorRateChart from './ErrorRateChart'
import TopEndpointsTable from './TopEndpointsTable';
import {
getServicesMetrics,
metricItem,
getTopEndpoints,
topEndpointListItem,
GlobalTime,
updateTimeInterval,
} from "../../actions";
import { StoreState } from "../../reducers";
import LatencyLineChart from "./LatencyLineChart";
import RequestRateChart from "./RequestRateChart";
import ErrorRateChart from "./ErrorRateChart";
import TopEndpointsTable from "./TopEndpointsTable";
const { TabPane } = Tabs;
interface ServicesMetricsProps extends RouteComponentProps<any>{
serviceMetrics: metricItem[],
getServicesMetrics: Function,
topEndpointsList: topEndpointListItem[],
getTopEndpoints: Function,
globalTime: GlobalTime,
updateTimeInterval: Function,
interface ServicesMetricsProps extends RouteComponentProps<any> {
serviceMetrics: metricItem[];
getServicesMetrics: Function;
topEndpointsList: topEndpointListItem[];
getTopEndpoints: Function;
globalTime: GlobalTime;
updateTimeInterval: Function;
}
const _ServiceMetrics = (props: ServicesMetricsProps) => {
const params = useParams<{ servicename?: string }>();
const params = useParams<{ servicename?: string; }>();
useEffect(() => {
props.getServicesMetrics(params.servicename, props.globalTime);
props.getTopEndpoints(params.servicename, props.globalTime);
}, [props.globalTime, params.servicename]);
useEffect( () => {
props.getServicesMetrics(params.servicename,props.globalTime);
props.getTopEndpoints(params.servicename,props.globalTime);
}, [props.globalTime,params.servicename]);
const onTracePopupClick = (timestamp: number) => {
props.updateTimeInterval("custom", [
timestamp / 1000000 - 5 * 60 * 1000,
timestamp / 1000000,
]); // updateTimeInterval takes second range in ms -- give -5 min to selected time,
props.history.push("/traces");
};
return (
<Tabs defaultActiveKey="1">
<TabPane tab="Application Metrics" key="1">
<Row gutter={32} style={{ margin: 20 }}>
<Col span={12}>
<Card bodyStyle={{ padding: 10 }}>
<LatencyLineChart
data={props.serviceMetrics}
popupClickHandler={onTracePopupClick}
/>
</Card>
</Col>
const onTracePopupClick = (timestamp:number) => {
<Col span={12}>
<Card bodyStyle={{ padding: 10 }}>
<RequestRateChart data={props.serviceMetrics} />
</Card>
</Col>
</Row>
props.updateTimeInterval('custom',[(timestamp/1000000)-5*60*1000,(timestamp/1000000)])// updateTimeInterval takes second range in ms -- give -5 min to selected time,
props.history.push('/traces')
}
return (
<Tabs defaultActiveKey="1">
<TabPane tab="Application Metrics" key="1">
<Row gutter={32} style={{ margin: 20 }}>
<Col span={12}>
<Card bodyStyle={{ padding: 10 }}>
<ErrorRateChart data={props.serviceMetrics} />
</Card>
</Col>
<Row gutter={32} style={{ margin: 20 }}>
<Col span={12} >
<Card bodyStyle={{padding:10}}>
<LatencyLineChart data={props.serviceMetrics} popupClickHandler={onTracePopupClick} />
</Card>
</Col>
<Col span={12}>
<Card bodyStyle={{ padding: 10 }}>
<TopEndpointsTable data={props.topEndpointsList} />
</Card>
</Col>
</Row>
</TabPane>
<Col span={12}>
<Card bodyStyle={{padding:10}}>
<RequestRateChart data={props.serviceMetrics} />
</Card>
</Col>
</Row>
<Row gutter={32} style={{ margin: 20 }}>
<Col span={12}>
<Card bodyStyle={{padding:10}}>
<ErrorRateChart data={props.serviceMetrics} />
</Card>
</Col>
<Col span={12}>
<Card bodyStyle={{padding:10}}>
<TopEndpointsTable data={props.topEndpointsList} />
</Card>
</Col>
</Row>
</TabPane>
<TabPane tab="External Calls" key="2">
<div style={{ margin: 20 }}> Coming Soon.. </div>
<div className="container" style={{ display: 'flex', flexFlow: 'column wrap' }}>
<div className='row'>
<div className='col-md-6 col-sm-12 col-12'>
{/* <ChartJSLineChart data={this.state.graphData} /> */}
</div>
<div className='col-md-6 col-sm-12 col-12'>
{/* <ChartJSLineChart data={this.state.graphData} /> */}
</div>
</div>
</div>
</TabPane>
</Tabs>
);
}
const mapStateToProps = (state: StoreState): { serviceMetrics: metricItem[], topEndpointsList: topEndpointListItem[],globalTime: GlobalTime} => {
return { serviceMetrics : state.serviceMetrics, topEndpointsList: state.topEndpointsList, globalTime:state.globalTime};
<TabPane tab="External Calls" key="2">
<div style={{ margin: 20 }}> Coming Soon.. </div>
<div
className="container"
style={{ display: "flex", flexFlow: "column wrap" }}
>
<div className="row">
<div className="col-md-6 col-sm-12 col-12">
{/* <ChartJSLineChart data={this.state.graphData} /> */}
</div>
<div className="col-md-6 col-sm-12 col-12">
{/* <ChartJSLineChart data={this.state.graphData} /> */}
</div>
</div>
</div>
</TabPane>
</Tabs>
);
};
export const ServiceMetrics = withRouter(connect(mapStateToProps, {
getServicesMetrics: getServicesMetrics,
getTopEndpoints: getTopEndpoints,
updateTimeInterval: updateTimeInterval,
})(_ServiceMetrics));
const mapStateToProps = (
state: StoreState,
): {
serviceMetrics: metricItem[];
topEndpointsList: topEndpointListItem[];
globalTime: GlobalTime;
} => {
return {
serviceMetrics: state.serviceMetrics,
topEndpointsList: state.topEndpointsList,
globalTime: state.globalTime,
};
};
export const ServiceMetrics = withRouter(
connect(mapStateToProps, {
getServicesMetrics: getServicesMetrics,
getTopEndpoints: getTopEndpoints,
updateTimeInterval: updateTimeInterval,
})(_ServiceMetrics),
);

View File

@ -1 +1 @@
export { ServiceMetrics as default } from './ServiceMetrics';
export { ServiceMetrics as default } from "./ServiceMetrics";

View File

@ -1,90 +1,123 @@
import React, {useEffect} from 'react';
import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { NavLink } from 'react-router-dom'
import { Table } from 'antd';
import styled from 'styled-components';
import { connect } from 'react-redux';
import { NavLink } from "react-router-dom";
import { Spin, Table } from "antd";
import styled from "styled-components";
import { connect } from "react-redux";
import { getServicesList, GlobalTime, servicesListItem } from '../../actions';
import { StoreState } from '../../reducers'
import { getServicesList, GlobalTime, servicesListItem } from "../../actions";
import { StoreState } from "../../reducers";
interface ServicesTableProps {
servicesList: servicesListItem[],
getServicesList: Function,
globalTime: GlobalTime,
servicesList: servicesListItem[];
getServicesList: Function;
globalTime: GlobalTime;
}
const Wrapper = styled.div`
padding-top:40px;
padding-bottom:40px;
padding-left:40px;
padding-right:40px;
.ant-table table { font-size: 12px; };
.ant-table tfoot>tr>td, .ant-table tfoot>tr>th, .ant-table-tbody>tr>td, .ant-table-thead>tr>th { padding: 10px; };
padding-top: 40px;
padding-bottom: 40px;
padding-left: 40px;
padding-right: 40px;
.ant-table table {
font-size: 12px;
}
.ant-table tfoot > tr > td,
.ant-table tfoot > tr > th,
.ant-table-tbody > tr > td,
.ant-table-thead > tr > th {
padding: 10px;
}
`;
const TableLoadingWrapper = styled.div`
display: flex;
justify-content: center;
margin-top: 80px;
`
const LoadingText = styled.div`
margin-left: 16px;
`
const columns = [
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
render: (text :string) => <NavLink style={{textTransform:'capitalize'}} to={'/application/' + text}><strong>{text}</strong></NavLink>,
},
{
title: 'P99 latency (in ms)',
dataIndex: 'p99',
key: 'p99',
sorter: (a:any, b:any) => a.p99 - b.p99,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'Error Rate (in %)',
dataIndex: 'errorRate',
key: 'errorRate',
sorter: (a:any, b:any) => a.errorRate - b.errorRate,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value*100).toFixed(2),
},
{
title: 'Requests Per Second',
dataIndex: 'callRate',
key: 'callRate',
sorter: (a:any, b:any) => a.callRate - b.callRate,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => value.toFixed(2),
},
{
title: "Application",
dataIndex: "serviceName",
key: "serviceName",
render: (text: string) => (
<NavLink style={{ textTransform: "capitalize" }} to={"/application/" + text}>
<strong>{text}</strong>
</NavLink>
),
},
{
title: "P99 latency (in ms)",
dataIndex: "p99",
key: "p99",
sorter: (a: any, b: any) => a.p99 - b.p99,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value / 1000000).toFixed(2),
},
{
title: "Error Rate (in %)",
dataIndex: "errorRate",
key: "errorRate",
sorter: (a: any, b: any) => a.errorRate - b.errorRate,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value * 100).toFixed(2),
},
{
title: "Requests Per Second",
dataIndex: "callRate",
key: "callRate",
sorter: (a: any, b: any) => a.callRate - b.callRate,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => value.toFixed(2),
},
];
const _ServicesTable = (props: ServicesTableProps) => {
const search = useLocation().search;
const time_interval = new URLSearchParams(search).get("time");
const [dataFetched, setDataFetched] = useState(false)
useEffect(() => {
/*
@Note - Change this from action to thunk
*/
props.getServicesList(props.globalTime).then(()=>{
setDataFetched(true)
}).catch((e:string)=>{
alert(e)
});
}, [props.globalTime]);
const search = useLocation().search;
const time_interval = new URLSearchParams(search).get('time');
if(!dataFetched){
return (
<TableLoadingWrapper>
<Spin/>
<LoadingText>Fetching data</LoadingText>
</TableLoadingWrapper>
)
}
useEffect( () => {
props.getServicesList(props.globalTime);
}, [props.globalTime]);
return (
<Wrapper>
<Table
dataSource={props.servicesList}
columns={columns}
pagination={false}
/>
</Wrapper>
);
};
const mapStateToProps = (
state: StoreState,
): { servicesList: servicesListItem[]; globalTime: GlobalTime } => {
return { servicesList: state.servicesList, globalTime: state.globalTime };
};
return(
<Wrapper>
<Table dataSource={props.servicesList} columns={columns} pagination={false} />
</Wrapper>
);
}
const mapStateToProps = (state: StoreState): { servicesList: servicesListItem[], globalTime: GlobalTime } => {
return { servicesList : state.servicesList, globalTime:state.globalTime};
};
export const ServicesTable = connect(mapStateToProps, {
getServicesList: getServicesList,
})(_ServicesTable);
export const ServicesTable = connect(mapStateToProps, {
getServicesList: getServicesList,
})(_ServicesTable);

View File

@ -1 +1 @@
export { ServicesTable as default } from './ServicesTable';
export { ServicesTable as default } from "./ServicesTable";

View File

@ -1,73 +1,76 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Table } from 'antd'
import styled from 'styled-components';
import { topEndpointListItem } from '../../actions/metrics';
import React from "react";
import { NavLink } from "react-router-dom";
import { Table } from "antd";
import styled from "styled-components";
import { topEndpointListItem } from "../../actions/metrics";
const Wrapper = styled.div`
padding-top:10px;
padding-bottom:10px;
padding-left:20px;
padding-right:20px;
.ant-table table { font-size: 12px; };
.ant-table tfoot>tr>td, .ant-table tfoot>tr>th, .ant-table-tbody>tr>td, .ant-table-thead>tr>th { padding: 10px; };
padding-top: 10px;
padding-bottom: 10px;
padding-left: 20px;
padding-right: 20px;
.ant-table table {
font-size: 12px;
}
.ant-table tfoot > tr > td,
.ant-table tfoot > tr > th,
.ant-table-tbody > tr > td,
.ant-table-thead > tr > th {
padding: 10px;
}
`;
interface TopEndpointsTableProps {
data : topEndpointListItem[],
data: topEndpointListItem[];
}
const TopEndpointsTable = (props: TopEndpointsTableProps) => {
const columns: any = [
{
title: "Name",
dataIndex: "name",
key: "name",
const columns: any = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text: string) => <NavLink to={"/" + text}>{text}</NavLink>,
},
{
title: "P50 (in ms)",
dataIndex: "p50",
key: "p50",
sorter: (a: any, b: any) => a.p50 - b.p50,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value / 1000000).toFixed(2),
},
{
title: "P90 (in ms)",
dataIndex: "p90",
key: "p90",
sorter: (a: any, b: any) => a.p90 - b.p90,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value / 1000000).toFixed(2),
},
{
title: "P99 (in ms)",
dataIndex: "p99",
key: "p99",
sorter: (a: any, b: any) => a.p99 - b.p99,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value / 1000000).toFixed(2),
},
{
title: "Number of Calls",
dataIndex: "numCalls",
key: "numCalls",
sorter: (a: any, b: any) => a.numCalls - b.numCalls,
},
];
render: (text :string) => <NavLink to={'/' + text}>{text}</NavLink>,
},
{
title: 'P50 (in ms)',
dataIndex: 'p50',
key: 'p50',
sorter: (a:any, b:any) => a.p50 - b.p50,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'P90 (in ms)',
dataIndex: 'p90',
key: 'p90',
sorter: (a:any, b:any) => a.p90 - b.p90,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'P99 (in ms)',
dataIndex: 'p99',
key: 'p99',
sorter: (a:any, b:any) => a.p99 - b.p99,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'Number of Calls',
dataIndex: 'numCalls',
key: 'numCalls',
sorter: (a:any, b:any) => a.numCalls - b.numCalls,
},
];
return(
<Wrapper>
<h6> Top Endpoints</h6>
<Table dataSource={props.data} columns={columns} pagination={false} />
</Wrapper>
)
}
return (
<Wrapper>
<h6> Top Endpoints</h6>
<Table dataSource={props.data} columns={columns} pagination={false} />
</Wrapper>
);
};
export default TopEndpointsTable;

View File

@ -10,62 +10,62 @@ import Graph from "react-graph-vis";
// Set shapes - https://visjs.github.io/vis-network/docs/network/nodes.html#
// https://github.com/crubier/react-graph-vis/issues/93
const graph = {
nodes: [
{ id: 1, label: "Catalogue", shape: "box", color: "green",border: "black",size: 100 },
{ id: 2, label: "Users", shape: "box", color: "#FFFF00" },
{ id: 3, label: "Payment App", shape: "box", color: "#FB7E81" },
{ id: 4, label: "My Sql", shape: "box", size: 10, color: "#7BE141" },
{ id: 5, label: "Redis-db", shape: "box", color: "#6E6EFD" },
],
edges: [
{from:1,to:2,color: { color: "red" },size:{size:20}},
{from:2,to:3,color: { color: "red" }},
{from:1,to:3,color: { color: "red" }},
{from:3,to:4,color: { color: "red" }},
{from:3,to:5,color: { color: "red" }},
]
};
nodes: [
{
id: 1,
label: "Catalogue",
shape: "box",
color: "green",
border: "black",
size: 100,
},
{ id: 2, label: "Users", shape: "box", color: "#FFFF00" },
{ id: 3, label: "Payment App", shape: "box", color: "#FB7E81" },
{ id: 4, label: "My Sql", shape: "box", size: 10, color: "#7BE141" },
{ id: 5, label: "Redis-db", shape: "box", color: "#6E6EFD" },
],
edges: [
{ from: 1, to: 2, color: { color: "red" }, size: { size: 20 } },
{ from: 2, to: 3, color: { color: "red" } },
{ from: 1, to: 3, color: { color: "red" } },
{ from: 3, to: 4, color: { color: "red" } },
{ from: 3, to: 5, color: { color: "red" } },
],
};
const options = {
layout: {
hierarchical: true
},
edges: {
color: "#000000"
},
height: "500px"
};
const options = {
layout: {
hierarchical: true,
},
edges: {
color: "#000000",
},
height: "500px",
};
// const events = {
// select: function(event:any) { //PNOTE - TO DO - Get rid of any type
// var { nodes, edges } = event;
// }
// };
// const events = {
// select: function(event:any) { //PNOTE - TO DO - Get rid of any type
// var { nodes, edges } = event;
// }
// };
const ServiceGraph = () => {
// const [network, setNetwork] = useState(null);
// const [network, setNetwork] = useState(null);
return (
<React.Fragment>
<div> Updated Service Graph module coming soon..</div>
<Graph
graph={graph}
options={options}
// events={events}
// getNetwork={network => {
// // if you want access to vis.js network api you can set the state in a parent component using this property
// }}
/>
</React.Fragment>
);
}
return (
<React.Fragment>
<div> Updated Service Graph module coming soon..</div>
<Graph
graph={graph}
options={options}
// events={events}
// getNetwork={network => {
// // if you want access to vis.js network api you can set the state in a parent component using this property
// }}
/>
</React.Fragment>
);
};
export default ServiceGraph;

View File

@ -1,19 +1,14 @@
import React from 'react';
import ServiceGraph from './ServiceGraph'
import React from "react";
import ServiceGraph from "./ServiceGraph";
const ServiceMap = () => {
return (
<div> Service Map module coming soon...
{/* <ServiceGraph /> */}
</div>
);
}
return (
<div>
{" "}
Service Map module coming soon...
{/* <ServiceGraph /> */}
</div>
);
};
export default ServiceMap;

View File

@ -1,77 +1,115 @@
import React from 'react';
import { Card, Tag } from 'antd';
import { connect } from 'react-redux';
import React from "react";
import { Card, Tag } from "antd";
import { connect } from "react-redux";
import { StoreState } from '../../reducers'
import { TagItem, TraceFilters, updateTraceFilters } from '../../actions';
import { StoreState } from "../../reducers";
import { TagItem, TraceFilters, updateTraceFilters } from "../../actions";
interface FilterStateDisplayProps {
traceFilters: TraceFilters,
updateTraceFilters: Function,
}
const _FilterStateDisplay = (props: FilterStateDisplayProps) => {
function handleCloseTag(value:string) {
if (value==='service')
props.updateTraceFilters({...props.traceFilters,service:''})
if (value==='operation')
props.updateTraceFilters({...props.traceFilters,operation:''})
if (value==='maxLatency')
props.updateTraceFilters({...props.traceFilters,latency:{'max':'','min':props.traceFilters.latency?.min}})
if (value==='minLatency')
props.updateTraceFilters({...props.traceFilters,latency:{'min':'','max':props.traceFilters.latency?.max}})
}
function handleCloseTagElement(item:TagItem){
props.updateTraceFilters({...props.traceFilters,tags:props.traceFilters.tags?.filter(elem => elem !== item)})
}
return(
<Card style={{padding: 6, marginTop: 10, marginBottom: 10}} bodyStyle={{padding: 6}}>
{(props.traceFilters.service===''||props.traceFilters.operation===undefined)? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('service');}}>
service:{props.traceFilters.service}
</Tag> }
{(props.traceFilters.operation===''||props.traceFilters.operation===undefined)? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('operation');}}>
operation:{props.traceFilters.operation}
</Tag> }
{props.traceFilters.latency===undefined||props.traceFilters.latency?.min===''? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('minLatency');}}>
minLatency:{(parseInt(props.traceFilters.latency!.min)/1000000).toString()}ms
</Tag> }
{props.traceFilters.latency===undefined||props.traceFilters.latency?.max===''? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('maxLatency');}}>
maxLatency:{(parseInt(props.traceFilters.latency!.max)/1000000).toString()}ms
</Tag> }
{props.traceFilters.tags === undefined? null: props.traceFilters.tags.map( item => (
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTagElement(item);}}>
{item.key} {item.operator} {item.value}
</Tag>))}
</Card>
);
traceFilters: TraceFilters;
updateTraceFilters: Function;
}
const mapStateToProps = (state: StoreState): { traceFilters: TraceFilters } => {
return { traceFilters : state.traceFilters };
const _FilterStateDisplay = (props: FilterStateDisplayProps) => {
function handleCloseTag(value: string) {
if (value === "service")
props.updateTraceFilters({ ...props.traceFilters, service: "" });
if (value === "operation")
props.updateTraceFilters({ ...props.traceFilters, operation: "" });
if (value === "maxLatency")
props.updateTraceFilters({
...props.traceFilters,
latency: { max: "", min: props.traceFilters.latency?.min },
});
if (value === "minLatency")
props.updateTraceFilters({
...props.traceFilters,
latency: { min: "", max: props.traceFilters.latency?.max },
});
}
function handleCloseTagElement(item: TagItem) {
props.updateTraceFilters({
...props.traceFilters,
tags: props.traceFilters.tags?.filter((elem) => elem !== item),
});
}
return (
<Card
style={{ padding: 6, marginTop: 10, marginBottom: 10 }}
bodyStyle={{ padding: 6 }}
>
{props.traceFilters.service === "" ||
props.traceFilters.operation === undefined ? null : (
<Tag
style={{ fontSize: 14, padding: 8 }}
closable
onClose={(e) => {
handleCloseTag("service");
}}
>
service:{props.traceFilters.service}
</Tag>
)}
{props.traceFilters.operation === "" ||
props.traceFilters.operation === undefined ? null : (
<Tag
style={{ fontSize: 14, padding: 8 }}
closable
onClose={(e) => {
handleCloseTag("operation");
}}
>
operation:{props.traceFilters.operation}
</Tag>
)}
{props.traceFilters.latency === undefined ||
props.traceFilters.latency?.min === "" ? null : (
<Tag
style={{ fontSize: 14, padding: 8 }}
closable
onClose={(e) => {
handleCloseTag("minLatency");
}}
>
minLatency:
{(parseInt(props.traceFilters.latency!.min) / 1000000).toString()}ms
</Tag>
)}
{props.traceFilters.latency === undefined ||
props.traceFilters.latency?.max === "" ? null : (
<Tag
style={{ fontSize: 14, padding: 8 }}
closable
onClose={(e) => {
handleCloseTag("maxLatency");
}}
>
maxLatency:
{(parseInt(props.traceFilters.latency!.max) / 1000000).toString()}ms
</Tag>
)}
{props.traceFilters.tags === undefined
? null
: props.traceFilters.tags.map((item) => (
<Tag
style={{ fontSize: 14, padding: 8 }}
closable
onClose={(e) => {
handleCloseTagElement(item);
}}
>
{item.key} {item.operator} {item.value}
</Tag>
))}
</Card>
);
};
const mapStateToProps = (state: StoreState): { traceFilters: TraceFilters } => {
return { traceFilters: state.traceFilters };
};
export const FilterStateDisplay = connect(mapStateToProps,
{
updateTraceFilters: updateTraceFilters,
})(_FilterStateDisplay);
export const FilterStateDisplay = connect(mapStateToProps, {
updateTraceFilters: updateTraceFilters,
})(_FilterStateDisplay);

View File

@ -1,66 +1,65 @@
import React from 'react';
import { Modal, Form, InputNumber, Col, Row} from 'antd';
import { Store } from 'antd/lib/form/interface';
import React from "react";
import { Modal, Form, InputNumber, Col, Row } from "antd";
import { Store } from "antd/lib/form/interface";
interface LatencyModalFormProps {
visible: boolean;
onCreate: (values: Store) => void; //Store is defined in antd forms library
onCancel: () => void;
visible: boolean;
onCreate: (values: Store) => void; //Store is defined in antd forms library
onCancel: () => void;
}
const LatencyModalForm: React.FC<LatencyModalFormProps> = ({
visible,
onCreate,
onCancel,
visible,
onCreate,
onCancel,
}) => {
const [form] = Form.useForm();
return (
<Modal
visible={visible}
title="Chose min and max values of Latency"
okText="Apply"
cancelText="Cancel"
onCancel={onCancel}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
onCreate(values); // giving error for values
})
.catch(info => {
console.log('Validate Failed:', info);
});
}}
>
<Form
form={form}
layout="horizontal"
name="form_in_modal"
initialValues={{ min: '100', max:'500' }}
>
<Row>
{/* <Input.Group compact> */}
<Col span={12}>
<Form.Item
name="min"
label="Min (in ms)"
// rules={[{ required: true, message: 'Please input the title of collection!' }]}
>
<InputNumber />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="max" label="Max (in ms)">
<InputNumber />
</Form.Item>
</Col>
</Row>
{/* </Input.Group> */}
</Form>
</Modal>
);
const [form] = Form.useForm();
return (
<Modal
visible={visible}
title="Chose min and max values of Latency"
okText="Apply"
cancelText="Cancel"
onCancel={onCancel}
onOk={() => {
form
.validateFields()
.then((values) => {
form.resetFields();
onCreate(values); // giving error for values
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
>
<Form
form={form}
layout="horizontal"
name="form_in_modal"
initialValues={{ min: "100", max: "500" }}
>
<Row>
{/* <Input.Group compact> */}
<Col span={12}>
<Form.Item
name="min"
label="Min (in ms)"
// rules={[{ required: true, message: 'Please input the title of collection!' }]}
>
<InputNumber />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="max" label="Max (in ms)">
<InputNumber />
</Form.Item>
</Col>
</Row>
{/* </Input.Group> */}
</Form>
</Modal>
);
};
export default LatencyModalForm;

View File

@ -1,46 +1,52 @@
import React from 'react';
import {Card,Tabs} from 'antd';
import React from "react";
import { Card, Tabs } from "antd";
const { TabPane } = Tabs;
interface spanTagItem {
key:string;
type:string;
value:string;
}
key: string;
type: string;
value: string;
}
interface SelectedSpanDetailsProps {
clickedSpanTags: spanTagItem[]
clickedSpanTags: spanTagItem[];
}
const SelectedSpanDetails = (props: SelectedSpanDetailsProps) => {
const callback = (key: any) => {};
const callback = (key:any) => {
}
return (
<Card style={{ height: 320 }} >
<Tabs defaultActiveKey="1" onChange={callback}>
<TabPane tab="Tags" key="1">
<strong> Details for selected Span </strong>
{props.clickedSpanTags.map((tags,index)=><li key={index} style={{color:'grey',fontSize:'13px',listStyle:'none'}}><span className='mr-1'>{tags.key}</span>:<span className='ml-1'>{tags.key==='error'?"true":tags.value}</span></li>)} </TabPane>
<TabPane tab="Errors" key="2">
{props.clickedSpanTags.filter(tags=>tags.key==='error').map(
error => <div className='ml-5'>
<p style={{color:'grey',fontSize:'10px'}}>
<span className='mr-1'>{error.key}</span>:
<span className='ml-1'>true</span></p>
</div>)}
</TabPane>
</Tabs>
</Card>
);
}
return (
<Card style={{ height: 320 }}>
<Tabs defaultActiveKey="1" onChange={callback}>
<TabPane tab="Tags" key="1">
<strong> Details for selected Span </strong>
{props.clickedSpanTags.map((tags, index) => (
<li
key={index}
style={{ color: "grey", fontSize: "13px", listStyle: "none" }}
>
<span className="mr-1">{tags.key}</span>:
<span className="ml-1">
{tags.key === "error" ? "true" : tags.value}
</span>
</li>
))}{" "}
</TabPane>
<TabPane tab="Errors" key="2">
{props.clickedSpanTags
.filter((tags) => tags.key === "error")
.map((error) => (
<div className="ml-5">
<p style={{ color: "grey", fontSize: "10px" }}>
<span className="mr-1">{error.key}</span>:
<span className="ml-1">true</span>
</p>
</div>
))}
</TabPane>
</Tabs>
</Card>
);
};
export default SelectedSpanDetails;

View File

@ -1,221 +1,238 @@
import React, {useState,useEffect} from 'react';
import GenericVisualizations from '../metrics/GenericVisualization'
import {Select, Card, Space, Form} from 'antd';
import { connect } from 'react-redux';
import React, { useState, useEffect } from "react";
import GenericVisualizations from "../metrics/GenericVisualization";
import { Select, Card, Space, Form } from "antd";
import { connect } from "react-redux";
import { StoreState } from '../../reducers'
import {customMetricsItem, getFilteredTraceMetrics, GlobalTime, TraceFilters} from '../../actions';
import { StoreState } from "../../reducers";
import {
customMetricsItem,
getFilteredTraceMetrics,
GlobalTime,
TraceFilters,
} from "../../actions";
const { Option } = Select;
const entity = [
{
title: 'Calls',
key:'calls',
dataindex:'calls'
},
{
title: 'Duration',
key:'duration',
dataindex:'duration'
},
{
title: 'Error',
key:'error',
dataindex:'error'
},
{
title: 'Status Code',
key:'status_code',
dataindex:'status_code'
},
{
title: "Calls",
key: "calls",
dataindex: "calls",
},
{
title: "Duration",
key: "duration",
dataindex: "duration",
},
{
title: "Error",
key: "error",
dataindex: "error",
},
{
title: "Status Code",
key: "status_code",
dataindex: "status_code",
},
];
const aggregation_options = [
{
linked_entity: 'calls',
default_selected:{title:'Count', dataindex:'count'},
options_available: [ {title:'Count', dataindex:'count'}, {title:'Rate (per sec)', dataindex:'rate_per_sec'}]
},
{
linked_entity: 'duration',
default_selected:{title:'p99', dataindex:'p99'},
// options_available: [ {title:'Avg', dataindex:'avg'}, {title:'Max', dataindex:'max'},{title:'Min', dataindex:'min'}, {title:'p50', dataindex:'p50'},{title:'p90', dataindex:'p90'}, {title:'p95', dataindex:'p95'}]
options_available: [ {title:'p50', dataindex:'p50'},{title:'p90', dataindex:'p90'}, {title:'p99', dataindex:'p99'}]
},
{
linked_entity: 'error',
default_selected:{title:'Count', dataindex:'count'},
options_available: [ {title:'Count', dataindex:'count'}, {title:'Rate (per sec)', dataindex:'rate_per_sec'}]
},
{
linked_entity: 'status_code',
default_selected: {title:'Count', dataindex:'count'},
options_available: [ {title:'Count', dataindex:'count'}]
},
const aggregation_options = [
{
linked_entity: "calls",
default_selected: { title: "Count", dataindex: "count" },
options_available: [
{ title: "Count", dataindex: "count" },
{ title: "Rate (per sec)", dataindex: "rate_per_sec" },
],
},
{
linked_entity: "duration",
default_selected: { title: "p99", dataindex: "p99" },
// options_available: [ {title:'Avg', dataindex:'avg'}, {title:'Max', dataindex:'max'},{title:'Min', dataindex:'min'}, {title:'p50', dataindex:'p50'},{title:'p90', dataindex:'p90'}, {title:'p95', dataindex:'p95'}]
options_available: [
{ title: "p50", dataindex: "p50" },
{ title: "p90", dataindex: "p90" },
{ title: "p99", dataindex: "p99" },
],
},
{
linked_entity: "error",
default_selected: { title: "Count", dataindex: "count" },
options_available: [
{ title: "Count", dataindex: "count" },
{ title: "Rate (per sec)", dataindex: "rate_per_sec" },
],
},
{
linked_entity: "status_code",
default_selected: { title: "Count", dataindex: "count" },
options_available: [{ title: "Count", dataindex: "count" }],
},
];
interface TraceCustomVisualizationsProps {
filteredTraceMetrics: customMetricsItem[],
globalTime: GlobalTime,
getFilteredTraceMetrics: Function,
traceFilters: TraceFilters,
}
const _TraceCustomVisualizations = (props: TraceCustomVisualizationsProps) => {
const [selectedEntity, setSelectedEntity] = useState('calls');
const [selectedAggOption, setSelectedAggOption] = useState('count');
const [selectedStep, setSelectedStep] = useState('60');
// Step should be multiples of 60, 60 -> 1 min
useEffect( () => {
let request_string= 'service='+props.traceFilters.service+
'&operation='+props.traceFilters.operation+
'&maxDuration='+props.traceFilters.latency?.max+
'&minDuration='+props.traceFilters.latency?.min
if(props.traceFilters.tags)
request_string=request_string+'&tags='+encodeURIComponent(JSON.stringify(props.traceFilters.tags));
if(selectedEntity)
request_string=request_string+'&dimension='+selectedEntity.toLowerCase();
if(selectedAggOption)
request_string=request_string+'&aggregation_option='+selectedAggOption.toLowerCase();
if(selectedStep)
request_string=request_string+'&step='+selectedStep;
props.getFilteredTraceMetrics(request_string, props.globalTime)
}, [selectedEntity,selectedAggOption,props.traceFilters, props.globalTime ]);
//Custom metrics API called if time, tracefilters, selected entity or agg option changes
const [form] = Form.useForm();
function handleChange(value:string) {
// console.log(value);
}
function handleFinish(value:string) {
// console.log(value);
}
// PNOTE - Can also use 'coordinate' option in antd Select for implementing this - https://ant.design/components/select/
const handleFormValuesChange = (changedValues:any) => {
const formFieldName = Object.keys(changedValues)[0];
if (formFieldName === 'entity') {
const temp_entity = aggregation_options.filter((item) => item.linked_entity === changedValues[formFieldName])[0];
form.setFieldsValue( {
agg_options : temp_entity.default_selected.title,
// PNOTE - TO DO Check if this has the same behaviour as selecting an option?
})
let temp = form.getFieldsValue(['agg_options','entity']);
setSelectedEntity(temp.entity);
setSelectedAggOption(temp.agg_options);
//form.setFieldsValue({ agg_options: aggregation_options.filter( item => item.linked_entity === selectedEntity )[0] }); //reset product selection
// PNOTE - https://stackoverflow.com/questions/64377293/update-select-option-list-based-on-other-select-field-selection-ant-design
}
if (formFieldName === 'agg_options') {
setSelectedAggOption(changedValues[formFieldName]);
}
}
return (
<Card>
{/* <Space direction="vertical"> */}
<div>Custom Visualizations</div>
<Form
form={form}
onFinish={handleFinish}
onValuesChange={handleFormValuesChange}
initialValues={{ agg_options: 'Count', chart_style:'line', interval:'5m', group_by:'none' }}
>
<Space>
<Form.Item name="entity">
<Select defaultValue={selectedEntity} style={{ width: 120 }} allowClear>
{entity.map((item) => (
<Option key={item.key} value={item.dataindex}>
{item.title}
</Option>
)
)
}
</Select>
</Form.Item>
<Form.Item name="agg_options">
<Select style={{ width: 120 }} allowClear>
{ aggregation_options.filter((item) => item.linked_entity === selectedEntity)[0].options_available
.map((item) => (
<Option key={item.dataindex} value={item.dataindex}>
{item.title}
</Option>
))
}
</Select>
</Form.Item>
<Form.Item name="chart_style">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="line">Line Chart</Option>
<Option value="bar">Bar Chart</Option>
<Option value="area">Area Chart</Option>
</Select>
</Form.Item>
<Form.Item name="interval">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="1m">1 min</Option>
<Option value="5m">5 min</Option>
<Option value="30m">30 min</Option>
</Select>
</Form.Item>
{/* Need heading for each option */}
<Form.Item name="group_by">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="none">Group By</Option>
<Option value="status">Status Code</Option>
<Option value="protocol">Protocol</Option>
</Select>
</Form.Item>
</Space>
</Form>
<GenericVisualizations chartType='line' data={props.filteredTraceMetrics}/>
{/* This component should take bar or line as an input */}
</Card>
);
filteredTraceMetrics: customMetricsItem[];
globalTime: GlobalTime;
getFilteredTraceMetrics: Function;
traceFilters: TraceFilters;
}
const mapStateToProps = (state: StoreState): { filteredTraceMetrics: customMetricsItem[] , globalTime: GlobalTime, traceFilters: TraceFilters} => {
return { filteredTraceMetrics : state.filteredTraceMetrics, globalTime: state.globalTime,traceFilters:state.traceFilters };
};
const _TraceCustomVisualizations = (props: TraceCustomVisualizationsProps) => {
const [selectedEntity, setSelectedEntity] = useState("calls");
const [selectedAggOption, setSelectedAggOption] = useState("count");
const [selectedStep, setSelectedStep] = useState("60");
// Step should be multiples of 60, 60 -> 1 min
useEffect(() => {
let request_string =
"service=" +
props.traceFilters.service +
"&operation=" +
props.traceFilters.operation +
"&maxDuration=" +
props.traceFilters.latency?.max +
"&minDuration=" +
props.traceFilters.latency?.min;
if (props.traceFilters.tags)
request_string =
request_string +
"&tags=" +
encodeURIComponent(JSON.stringify(props.traceFilters.tags));
if (selectedEntity)
request_string =
request_string + "&dimension=" + selectedEntity.toLowerCase();
if (selectedAggOption)
request_string =
request_string + "&aggregation_option=" + selectedAggOption.toLowerCase();
if (selectedStep) request_string = request_string + "&step=" + selectedStep;
export const TraceCustomVisualizations = connect(mapStateToProps, {
getFilteredTraceMetrics: getFilteredTraceMetrics,
})(_TraceCustomVisualizations);
props.getFilteredTraceMetrics(request_string, props.globalTime);
}, [selectedEntity, selectedAggOption, props.traceFilters, props.globalTime]);
//Custom metrics API called if time, tracefilters, selected entity or agg option changes
const [form] = Form.useForm();
function handleChange(value: string) {
// console.log(value);
}
function handleFinish(value: string) {
// console.log(value);
}
// PNOTE - Can also use 'coordinate' option in antd Select for implementing this - https://ant.design/components/select/
const handleFormValuesChange = (changedValues: any) => {
const formFieldName = Object.keys(changedValues)[0];
if (formFieldName === "entity") {
const temp_entity = aggregation_options.filter(
(item) => item.linked_entity === changedValues[formFieldName],
)[0];
form.setFieldsValue({
agg_options: temp_entity.default_selected.title,
// PNOTE - TO DO Check if this has the same behaviour as selecting an option?
});
let temp = form.getFieldsValue(["agg_options", "entity"]);
setSelectedEntity(temp.entity);
setSelectedAggOption(temp.agg_options);
//form.setFieldsValue({ agg_options: aggregation_options.filter( item => item.linked_entity === selectedEntity )[0] }); //reset product selection
// PNOTE - https://stackoverflow.com/questions/64377293/update-select-option-list-based-on-other-select-field-selection-ant-design
}
if (formFieldName === "agg_options") {
setSelectedAggOption(changedValues[formFieldName]);
}
};
return (
<Card>
{/* <Space direction="vertical"> */}
<div>Custom Visualizations</div>
<Form
form={form}
onFinish={handleFinish}
onValuesChange={handleFormValuesChange}
initialValues={{
agg_options: "Count",
chart_style: "line",
interval: "5m",
group_by: "none",
}}
>
<Space>
<Form.Item name="entity">
<Select defaultValue={selectedEntity} style={{ width: 120 }} allowClear>
{entity.map((item) => (
<Option key={item.key} value={item.dataindex}>
{item.title}
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="agg_options">
<Select style={{ width: 120 }} allowClear>
{aggregation_options
.filter((item) => item.linked_entity === selectedEntity)[0]
.options_available.map((item) => (
<Option key={item.dataindex} value={item.dataindex}>
{item.title}
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="chart_style">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="line">Line Chart</Option>
<Option value="bar">Bar Chart</Option>
<Option value="area">Area Chart</Option>
</Select>
</Form.Item>
<Form.Item name="interval">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="1m">1 min</Option>
<Option value="5m">5 min</Option>
<Option value="30m">30 min</Option>
</Select>
</Form.Item>
{/* Need heading for each option */}
<Form.Item name="group_by">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="none">Group By</Option>
<Option value="status">Status Code</Option>
<Option value="protocol">Protocol</Option>
</Select>
</Form.Item>
</Space>
</Form>
<GenericVisualizations chartType="line" data={props.filteredTraceMetrics} />
{/* This component should take bar or line as an input */}
</Card>
);
};
const mapStateToProps = (
state: StoreState,
): {
filteredTraceMetrics: customMetricsItem[];
globalTime: GlobalTime;
traceFilters: TraceFilters;
} => {
return {
filteredTraceMetrics: state.filteredTraceMetrics,
globalTime: state.globalTime,
traceFilters: state.traceFilters,
};
};
export const TraceCustomVisualizations = connect(mapStateToProps, {
getFilteredTraceMetrics: getFilteredTraceMetrics,
})(_TraceCustomVisualizations);

View File

@ -1,22 +1,16 @@
import React from 'react';
import {TraceCustomVisualizations} from './TraceCustomVisualizations';
import { TraceFilter } from './TraceFilter';
import { TraceList } from './TraceList';
import React from "react";
import { TraceCustomVisualizations } from "./TraceCustomVisualizations";
import { TraceFilter } from "./TraceFilter";
import { TraceList } from "./TraceList";
const TraceDetail = () => {
return (
<div>
<TraceFilter />
<TraceCustomVisualizations />
<TraceList />
</div>
);
}
return (
<div>
<TraceFilter />
<TraceCustomVisualizations />
<TraceList />
</div>
);
};
export default TraceDetail;

View File

@ -1,293 +1,393 @@
import React,{useEffect, useState} from 'react';
import { Select, Button, Input, Form, AutoComplete} from 'antd';
import { connect } from 'react-redux';
import { Store } from 'antd/lib/form/interface';
import styled from 'styled-components';
import React, { useEffect, useState } from "react";
import { Select, Button, Input, Form, AutoComplete } from "antd";
import { connect } from "react-redux";
import { Store } from "antd/lib/form/interface";
import styled from "styled-components";
import { updateTraceFilters, fetchTraces, TraceFilters, GlobalTime } from '../../actions';
import { StoreState } from '../../reducers';
import LatencyModalForm from './LatencyModalForm';
import {FilterStateDisplay} from './FilterStateDisplay';
import FormItem from 'antd/lib/form/FormItem';
import metricsAPI from '../../api/metricsAPI';
import {
updateTraceFilters,
fetchTraces,
TraceFilters,
GlobalTime,
} from "../../actions";
import { StoreState } from "../../reducers";
import LatencyModalForm from "./LatencyModalForm";
import { FilterStateDisplay } from "./FilterStateDisplay";
import FormItem from "antd/lib/form/FormItem";
import metricsAPI from "../../api/metricsAPI";
const { Option } = Select;
const InfoWrapper = styled.div`
padding-top:10px;
font-style:italic;
font-size: 12px;
padding-top: 10px;
font-style: italic;
font-size: 12px;
`;
interface TraceFilterProps {
traceFilters: TraceFilters,
globalTime: GlobalTime,
updateTraceFilters: Function,
fetchTraces: Function,
traceFilters: TraceFilters;
globalTime: GlobalTime;
updateTraceFilters: Function;
fetchTraces: Function;
}
interface TagKeyOptionItem {
"tagKeys": string;
"tagCount": number;
tagKeys: string;
tagCount: number;
}
const _TraceFilter = (props: TraceFilterProps) => {
const [serviceList, setServiceList] = useState<string[]>([]);
const [operationList, setOperationsList] = useState<string[]>([]);
const [tagKeyOptions, setTagKeyOptions] = useState<TagKeyOptionItem[]>([]);
const [serviceList, setServiceList] = useState<string[]>([]);
const [operationList, setOperationsList] = useState<string[]>([]);
const [tagKeyOptions, setTagKeyOptions] = useState<TagKeyOptionItem[]>([]);
useEffect(() => {
metricsAPI.get<string[]>("services/list").then((response) => {
setServiceList(response.data);
});
}, []);
useEffect(() => {
let request_string =
"service=" +
props.traceFilters.service +
"&operation=" +
props.traceFilters.operation +
"&maxDuration=" +
props.traceFilters.latency?.max +
"&minDuration=" +
props.traceFilters.latency?.min;
if (props.traceFilters.tags)
request_string =
request_string +
"&tags=" +
encodeURIComponent(JSON.stringify(props.traceFilters.tags));
useEffect( () => {
metricsAPI.get<string[]>('services/list').then(response => {
setServiceList( response.data );
});
}, []);
props.fetchTraces(props.globalTime, request_string);
}, [props.traceFilters, props.globalTime]);
useEffect( () => {
let request_string='service='+props.traceFilters.service+
'&operation='+props.traceFilters.operation+
'&maxDuration='+props.traceFilters.latency?.max+
'&minDuration='+props.traceFilters.latency?.min
if(props.traceFilters.tags)
request_string=request_string+'&tags='+encodeURIComponent(JSON.stringify(props.traceFilters.tags));
useEffect(() => {
let latencyButtonText = "Latency";
if (
props.traceFilters.latency?.min === "" &&
props.traceFilters.latency?.max !== ""
)
latencyButtonText =
"Latency<" +
(parseInt(props.traceFilters.latency?.max) / 1000000).toString() +
"ms";
else if (
props.traceFilters.latency?.min !== "" &&
props.traceFilters.latency?.max === ""
)
latencyButtonText =
"Latency>" +
(parseInt(props.traceFilters.latency?.min) / 1000000).toString() +
"ms";
else if (
props.traceFilters.latency !== undefined &&
props.traceFilters.latency?.min !== "" &&
props.traceFilters.latency?.max !== ""
)
latencyButtonText =
(parseInt(props.traceFilters.latency.min) / 1000000).toString() +
"ms <Latency<" +
(parseInt(props.traceFilters.latency.max) / 1000000).toString() +
"ms";
props.fetchTraces(props.globalTime, request_string)
}, [props.traceFilters,props.globalTime]);
form_basefilter.setFieldsValue({ latency: latencyButtonText });
}, [props.traceFilters.latency]);
useEffect(() => {
form_basefilter.setFieldsValue({ service: props.traceFilters.service });
}, [props.traceFilters.service]);
useEffect ( () => {
useEffect(() => {
form_basefilter.setFieldsValue({ operation: props.traceFilters.operation });
}, [props.traceFilters.operation]);
let latencyButtonText = 'Latency';
if (props.traceFilters.latency?.min === '' && props.traceFilters.latency?.max !== '')
latencyButtonText = 'Latency<'+(parseInt(props.traceFilters.latency?.max)/1000000).toString()+'ms';
else if (props.traceFilters.latency?.min !== '' && props.traceFilters.latency?.max === '')
latencyButtonText = 'Latency>'+(parseInt(props.traceFilters.latency?.min)/1000000).toString()+'ms';
else if ( props.traceFilters.latency !== undefined && props.traceFilters.latency?.min !== '' && props.traceFilters.latency?.max !== '')
latencyButtonText = (parseInt(props.traceFilters.latency.min)/1000000).toString()+'ms <Latency<'+(parseInt(props.traceFilters.latency.max)/1000000).toString()+'ms';
const [modalVisible, setModalVisible] = useState(false);
const [loading] = useState(false);
const [tagKeyValueApplied, setTagKeyValueApplied] = useState([""]);
const [latencyFilterValues, setLatencyFilterValues] = useState({
min: "",
max: "",
});
form_basefilter.setFieldsValue({latency:latencyButtonText ,})
const [form] = Form.useForm();
}, [props.traceFilters.latency])
const [form_basefilter] = Form.useForm();
useEffect ( () => {
function handleChange(value: string) {
console.log(value);
}
form_basefilter.setFieldsValue({service: props.traceFilters.service,})
function handleChangeOperation(value: string) {
props.updateTraceFilters({ ...props.traceFilters, operation: value });
}
}, [props.traceFilters.service])
function handleChangeService(value: string) {
let service_request = "/service/" + value + "/operations";
metricsAPI.get<string[]>(service_request).then((response) => {
// form_basefilter.resetFields(['operation',])
setOperationsList(response.data);
});
useEffect ( () => {
let tagkeyoptions_request = "tags?service=" + value;
metricsAPI.get<TagKeyOptionItem[]>(tagkeyoptions_request).then((response) => {
setTagKeyOptions(response.data);
});
form_basefilter.setFieldsValue({operation: props.traceFilters.operation,})
props.updateTraceFilters({ ...props.traceFilters, service: value });
}
}, [props.traceFilters.operation])
const onLatencyButtonClick = () => {
setModalVisible(true);
};
const [modalVisible, setModalVisible] = useState(false);
const [loading] = useState(false);
const onLatencyModalApply = (values: Store) => {
setModalVisible(false);
props.updateTraceFilters({
...props.traceFilters,
latency: {
min: values.min ? (parseInt(values.min) * 1000000).toString() : "",
max: values.max ? (parseInt(values.max) * 1000000).toString() : "",
},
});
};
const [tagKeyValueApplied, setTagKeyValueApplied]=useState(['']);
const [latencyFilterValues, setLatencyFilterValues]=useState({min:'',max:''})
const onTagFormSubmit = (values: any) => {
let request_tags =
"service=frontend&tags=" +
encodeURIComponent(
JSON.stringify([
{
key: values.tag_key,
value: values.tag_value,
operator: values.operator,
},
]),
);
const [form] = Form.useForm();
if (props.traceFilters.tags) {
// If there are existing tag filters present
props.updateTraceFilters({
service: props.traceFilters.service,
operation: props.traceFilters.operation,
latency: props.traceFilters.latency,
tags: [
...props.traceFilters.tags,
{
key: values.tag_key,
value: values.tag_value,
operator: values.operator,
},
],
});
} else {
props.updateTraceFilters({
service: props.traceFilters.service,
operation: props.traceFilters.operation,
latency: props.traceFilters.latency,
tags: [
{
key: values.tag_key,
value: values.tag_value,
operator: values.operator,
},
],
});
}
const [form_basefilter] = Form.useForm();
form.resetFields();
};
function handleChange(value:string) {
console.log(value);
}
const onTagClose = (value: string) => {
setTagKeyValueApplied(tagKeyValueApplied.filter((e) => e !== value));
};
function handleChangeOperation(value:string) {
props.updateTraceFilters({...props.traceFilters,operation:value})
}
// For autocomplete
//Setting value when autocomplete field is changed
const onChangeTagKey = (data: string) => {
form.setFieldsValue({ tag_key: data });
};
function handleChangeService(value:string) {
let service_request='/service/'+value+'/operations';
metricsAPI.get<string[]>(service_request).then(response => {
// form_basefilter.resetFields(['operation',])
setOperationsList( response.data );
});
const dataSource = ["status:200"];
const children = [];
for (let i = 0; i < dataSource.length; i++) {
children.push(
<Option value={dataSource[i]} key={dataSource[i]}>
{dataSource[i]}
</Option>,
);
}
let tagkeyoptions_request='tags?service='+value;
metricsAPI.get<TagKeyOptionItem[]>(tagkeyoptions_request).then(response => {
setTagKeyOptions( response.data );
});
// PNOTE - Remove any
const handleApplyFilterForm = (values: any) => {
let request_params: string = "";
if (
typeof values.service !== undefined &&
typeof values.operation !== undefined
) {
request_params =
"service=" + values.service + "&operation=" + values.operation;
} else if (
typeof values.service === undefined &&
typeof values.operation !== undefined
) {
request_params = "operation=" + values.operation;
} else if (
typeof values.service !== undefined &&
typeof values.operation === undefined
) {
request_params = "service=" + values.service;
}
props.updateTraceFilters({...props.traceFilters,service:value})
request_params =
request_params +
"&minDuration=" +
latencyFilterValues.min +
"&maxDuration=" +
latencyFilterValues.max;
}
setTagKeyValueApplied((tagKeyValueApplied) => [
...tagKeyValueApplied,
"service eq" + values.service,
"operation eq " + values.operation,
"maxduration eq " + (parseInt(latencyFilterValues.max) / 1000000).toString(),
"minduration eq " + (parseInt(latencyFilterValues.min) / 1000000).toString(),
]);
props.updateTraceFilters({
service: values.service,
operation: values.operation,
latency: latencyFilterValues,
});
};
const onLatencyButtonClick = () => {
setModalVisible(true);
}
return (
<div>
<div>Filter Traces</div>
{/* <div>{JSON.stringify(props.traceFilters)}</div> */}
<Form
form={form_basefilter}
layout="inline"
onFinish={handleApplyFilterForm}
initialValues={{ service: "", operation: "", latency: "Latency" }}
style={{ marginTop: 10, marginBottom: 10 }}
>
<FormItem rules={[{ required: true }]} name="service">
<Select
showSearch
style={{ width: 180 }}
onChange={handleChangeService}
placeholder="Select Service"
allowClear
>
{serviceList.map((s) => (
<Option value={s}>{s}</Option>
))}
</Select>
</FormItem>
const onLatencyModalApply = (values: Store) => {
setModalVisible(false);
props.updateTraceFilters({...props.traceFilters,latency:{min:values.min?(parseInt(values.min)*1000000).toString():"", max:values.max?(parseInt(values.max)*1000000).toString():""}})
<FormItem name="operation">
<Select
showSearch
style={{ width: 180 }}
onChange={handleChangeOperation}
placeholder="Select Operation"
allowClear
>
{operationList.map((item) => (
<Option value={item}>{item}</Option>
))}
</Select>
</FormItem>
}
<FormItem name="latency">
<Input
style={{ width: 200 }}
type="button"
onClick={onLatencyButtonClick}
/>
</FormItem>
const onTagFormSubmit = (values:any) => {
let request_tags= 'service=frontend&tags='+encodeURIComponent(JSON.stringify([{"key":values.tag_key,"value":values.tag_value,"operator":values.operator}]))
if (props.traceFilters.tags){ // If there are existing tag filters present
props.updateTraceFilters(
{
service:props.traceFilters.service,
operation:props.traceFilters.operation,
latency:props.traceFilters.latency,
tags:[...props.traceFilters.tags, {'key':values.tag_key,'value':values.tag_value,'operator':values.operator}]
});
}
else
{
props.updateTraceFilters(
{
service:props.traceFilters.service,
operation:props.traceFilters.operation,
latency:props.traceFilters.latency,
tags:[ {'key':values.tag_key,'value':values.tag_value,'operator':values.operator}]
});
}
form.resetFields();
}
const onTagClose = (value:string) => {
setTagKeyValueApplied(tagKeyValueApplied.filter( e => (e !== value)));
}
// For autocomplete
//Setting value when autocomplete field is changed
const onChangeTagKey = (data: string) => {
form.setFieldsValue({ tag_key: data });
};
const dataSource = ['status:200'];
const children = [];
for (let i = 0; i < dataSource.length; i++) {
children.push(<Option value={dataSource[i]} key={dataSource[i]}>{dataSource[i]}</Option>);
}
// PNOTE - Remove any
const handleApplyFilterForm = (values:any) => {
let request_params: string ='';
if (typeof values.service !== undefined && typeof(values.operation) !== undefined)
{
request_params = 'service='+values.service+'&operation='+values.operation;
}
else if (typeof values.service === undefined && typeof values.operation !== undefined)
{
request_params = 'operation='+values.operation;
}
else if (typeof values.service !== undefined && typeof values.operation === undefined)
{
request_params = 'service='+values.service;
}
request_params=request_params+'&minDuration='+latencyFilterValues.min+'&maxDuration='+latencyFilterValues.max;
setTagKeyValueApplied(tagKeyValueApplied => [...tagKeyValueApplied, 'service eq'+values.service, 'operation eq '+values.operation, 'maxduration eq '+ (parseInt(latencyFilterValues.max)/1000000).toString(), 'minduration eq '+(parseInt(latencyFilterValues.min)/1000000).toString()]);
props.updateTraceFilters({'service':values.service,'operation':values.operation,'latency':latencyFilterValues})
}
return (
<div>
<div>Filter Traces</div>
{/* <div>{JSON.stringify(props.traceFilters)}</div> */}
<Form form={form_basefilter} layout='inline' onFinish={handleApplyFilterForm} initialValues={{ service:'', operation:'',latency:'Latency',}} style={{marginTop: 10, marginBottom:10}}>
<FormItem rules={[{ required: true }]} name='service'>
<Select showSearch style={{ width: 180 }} onChange={handleChangeService} placeholder='Select Service' allowClear>
{serviceList.map( s => <Option value={s}>{s}</Option>)}
</Select>
</FormItem>
<FormItem name='operation'>
<Select showSearch style={{ width: 180 }} onChange={handleChangeOperation} placeholder='Select Operation' allowClear>
{operationList.map( item => <Option value={item}>{item}</Option>)}
</Select>
</FormItem>
<FormItem name='latency'>
<Input style={{ width: 200 }} type='button' onClick={onLatencyButtonClick}/>
</FormItem>
{/* <FormItem>
{/* <FormItem>
<Button type="primary" htmlType="submit">Apply Filters</Button>
</FormItem> */}
</Form>
</Form>
<FilterStateDisplay />
<FilterStateDisplay />
{/* // What will be the empty state of card when there is no Tag , it should show something */}
{/* // What will be the empty state of card when there is no Tag , it should show something */}
<InfoWrapper>Select Service to get Tag suggestions </InfoWrapper>
<InfoWrapper>Select Service to get Tag suggestions </InfoWrapper>
<Form
form={form}
layout="inline"
onFinish={onTagFormSubmit}
initialValues={{ operator: "equals" }}
style={{ marginTop: 10, marginBottom: 10 }}
>
<FormItem rules={[{ required: true }]} name="tag_key">
<AutoComplete
options={tagKeyOptions.map((s) => {
return { value: s.tagKeys };
})}
style={{ width: 200, textAlign: "center" }}
// onSelect={onSelect}
// onSearch={onSearch}
onChange={onChangeTagKey}
filterOption={(inputValue, option) =>
option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
placeholder="Tag Key"
/>
</FormItem>
<Form form={form} layout='inline' onFinish={onTagFormSubmit} initialValues={{operator:'equals'}} style={{marginTop: 10, marginBottom:10}}>
<FormItem name="operator">
<Select style={{ width: 120, textAlign: "center" }}>
<Option value="equals">EQUAL</Option>
<Option value="contains">CONTAINS</Option>
</Select>
</FormItem>
<FormItem rules={[{ required: true }]} name='tag_key'>
<FormItem rules={[{ required: true }]} name="tag_value">
<Input
style={{ width: 160, textAlign: "center" }}
placeholder="Tag Value"
/>
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit">
{" "}
Apply Tag Filter{" "}
</Button>
</FormItem>
</Form>
<AutoComplete
options={tagKeyOptions.map((s) => { return ({'value' : s.tagKeys}) })}
style={{ width: 200, textAlign: 'center' }}
// onSelect={onSelect}
// onSearch={onSearch}
onChange={onChangeTagKey}
filterOption={(inputValue, option) =>
option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
placeholder="Tag Key"
/>
</FormItem>
<LatencyModalForm
visible={modalVisible}
onCreate={onLatencyModalApply}
onCancel={() => {
setModalVisible(false);
}}
/>
</div>
);
};
<FormItem name='operator'>
<Select style={{ width: 120, textAlign: 'center' }}>
<Option value="equals">EQUAL</Option>
<Option value="contains">CONTAINS</Option>
</Select>
</FormItem>
<FormItem rules={[{ required: true }]} name='tag_value'>
<Input style={{ width: 160, textAlign: 'center',}} placeholder="Tag Value" />
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit"> Apply Tag Filter </Button>
</FormItem>
</Form>
<LatencyModalForm
visible={modalVisible}
onCreate={onLatencyModalApply}
onCancel={() => {
setModalVisible(false);
}}
/>
</div>
);
}
const mapStateToProps = (state: StoreState): { traceFilters: TraceFilters, globalTime: GlobalTime } => {
return { traceFilters: state.traceFilters, globalTime: state.globalTime };
const mapStateToProps = (
state: StoreState,
): { traceFilters: TraceFilters; globalTime: GlobalTime } => {
return { traceFilters: state.traceFilters, globalTime: state.globalTime };
};
export const TraceFilter = connect(mapStateToProps, {
updateTraceFilters: updateTraceFilters,
fetchTraces: fetchTraces,
updateTraceFilters: updateTraceFilters,
fetchTraces: fetchTraces,
})(_TraceFilter);

View File

@ -1,46 +1,46 @@
.d3-tip {
line-height: 1;
padding: 2px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
font-size: 6px;
line-height: 1;
padding: 2px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
font-size: 6px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 6px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
box-sizing: border-box;
display: inline;
font-size: 6px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* Style northward tooltips differently */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
/* SVG element */
/* Way to add borders in SVG - https://stackoverflow.com/questions/18330344/how-to-add-border-outline-stroke-to-svg-elements-in-css */
.frame {
fill: none;
stroke: rgba(255, 255, 255, 0.25);
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
stroke: rgba(255, 255, 255, 0.25);
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Transparency simulates sub pixel border https://stackoverflow.com/questions/13891177/css-border-less-than-1px */
.d3-flame-graph-label:hover {
border: 1px dotted;
border-color: rgba(255, 255, 255, 0.75);
border: 1px dotted;
border-color: rgba(255, 255, 255, 0.75);
}
/*
.d3-flame-graph-label:hover {

View File

@ -1,108 +1,105 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { flamegraph } from 'd3-flame-graph'
import { connect } from 'react-redux';
import { Card, Button, Row, Col, Space } from 'antd';
import * as d3 from 'd3';
import * as d3Tip from 'd3-tip';
import { flamegraph } from "d3-flame-graph";
import { connect } from "react-redux";
import { Card, Button, Row, Col, Space } from "antd";
import * as d3 from "d3";
import * as d3Tip from "d3-tip";
//import * as d3Tip from 'd3-tip';
// PNOTE - uninstall @types/d3-tip. issues with importing d3-tip https://github.com/Caged/d3-tip/issues/181
import './TraceGraph.css'
import { spanToTreeUtil } from '../../utils/spanToTree'
import { fetchTraceItem , spansWSameTraceIDResponse } from '../../actions';
import { StoreState } from '../../reducers'
import { TraceGraphColumn } from './TraceGraphColumn'
import SelectedSpanDetails from './SelectedSpanDetails'
import "./TraceGraph.css";
import { spanToTreeUtil } from "../../utils/spanToTree";
import { fetchTraceItem, spansWSameTraceIDResponse } from "../../actions";
import { StoreState } from "../../reducers";
import { TraceGraphColumn } from "./TraceGraphColumn";
import SelectedSpanDetails from "./SelectedSpanDetails";
interface TraceGraphProps {
traceItem: spansWSameTraceIDResponse ,
fetchTraceItem: Function,
traceItem: spansWSameTraceIDResponse;
fetchTraceItem: Function;
}
const _TraceGraph = (props: TraceGraphProps) => {
const params = useParams<{ id?: string }>();
const [clickedSpanTags, setClickedSpanTags] = useState([]);
const [resetZoom, setResetZoom] = useState(false);
const params = useParams<{ id?: string; }>();
const [clickedSpanTags,setClickedSpanTags]=useState([])
useEffect(() => {
//sets span width based on value - which is mapped to duration
props.fetchTraceItem(params.id);
}, []);
useEffect( () => {
props.fetchTraceItem(params.id);
}, []);
useEffect(() => {
if (props.traceItem || resetZoom) {
const tree = spanToTreeUtil(props.traceItem[0].events);
// This is causing element to change ref. Can use both useRef or this approach.
d3.select("#chart").datum(tree).call(chart);
setResetZoom(false);
}
}, [props.traceItem, resetZoom]);
// if this monitoring of props.traceItem.data is removed then zoom on click doesn't work
// Doesn't work if only do initial check, works if monitor an element - as it may get updated in sometime
useEffect( () => {
if (props.traceItem[0].events.length>0)
{
const tree = spanToTreeUtil(props.traceItem[0].events);
d3.select("#chart").datum(tree).call(chart);
}
},[props.traceItem]);
// if this monitoring of props.traceItem.data is removed then zoom on click doesn't work
// Doesn't work if only do initial check, works if monitor an element - as it may get updated in sometime
const tip = d3Tip
.default()
.attr("class", "d3-tip")
.html(function (d: any) {
return d.data.name + "<br>duration: " + d.data.value;
});
const onClick = (z: any) => {
setClickedSpanTags(z.data.tags);
console.log(`Clicked on ${z.data.name}, id: "${z.id}"`);
};
const chart = flamegraph()
.width(640)
.cellHeight(18)
.transitionDuration(500)
.inverted(true)
.tooltip(tip)
.minFrameSize(10)
.elided(false)
.differential(false)
.sort(true)
//Use self value=true when we're using not using aggregated option, Which is not our case.
// In that case it's doing step function sort of stuff thru computation.
// Source flamegraph.js line 557 and 573.
// .selfValue(true)
.onClick(onClick)
.title("Trace Flame graph");
const tip = d3Tip.default().attr('class', 'd3-tip').html(function(d:any) { return d.data.name+'<br>duration: '+d.data.value});
return (
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
<Col md={8} sm={24}>
<TraceGraphColumn />
</Col>
<Col md={16} sm={24}>
{/* <Card style={{ width: 640 }}> */}
<Space direction="vertical" size="middle">
<Card bodyStyle={{ padding: 80 }} style={{ height: 320 }}>
<div>Trace Graph component ID is {params.id} </div>
<Button type="primary" onClick={setResetZoom.bind(this, true)}>
Reset Zoom
</Button>
<div id="chart" style={{ fontSize: 12, marginTop: 20}}></div>
</Card>
const onClick = (z:any) => {
setClickedSpanTags(z.data.tags);
console.log(`Clicked on ${z.data.name}, id: "${z.id}"`);
}
const chart = flamegraph()
.width(640)
.cellHeight(18)
.transitionDuration(500)
.minFrameSize(5)
.sort(true)
.inverted(true)
.tooltip(tip)
.elided(false)
.onClick(onClick)
// .title("Trace Flame graph")
.differential(false)
.selfValue(true); //sets span width based on value - which is mapped to duration
const resetZoom = () => {
chart.resetZoom();
}
return (
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
<Col md={8} sm={24} >
<TraceGraphColumn />
</Col>
<Col md={16} sm={24} >
{/* <Card style={{ width: 640 }}> */}
<Space direction="vertical" size='middle' >
<Card bodyStyle={{padding: 80, }} style={{ height: 320, }}>
<div>Trace Graph component ID is {params.id} </div>
<Button type="primary" onClick={resetZoom}>Reset Zoom</Button>
<div id="chart" style={{ fontSize: 12 }}></div>
</Card>
<SelectedSpanDetails clickedSpanTags={clickedSpanTags}/>
</Space>
</Col>
</Row>
);
}
const mapStateToProps = (state: StoreState): { traceItem: spansWSameTraceIDResponse } => {
return { traceItem: state.traceItem };
<SelectedSpanDetails clickedSpanTags={clickedSpanTags} />
</Space>
</Col>
</Row>
);
};
const mapStateToProps = (
state: StoreState,
): { traceItem: spansWSameTraceIDResponse } => {
return { traceItem: state.traceItem };
};
export const TraceGraph = connect(mapStateToProps, {
fetchTraceItem: fetchTraceItem,
fetchTraceItem: fetchTraceItem,
})(_TraceGraph);

View File

@ -1,73 +1,77 @@
import React from 'react';
import { connect } from 'react-redux';
import { Table } from 'antd'
import { traceResponseNew, pushDStree } from '../../actions';
import { StoreState } from '../../reducers'
import React from "react";
import { connect } from "react-redux";
import { Table } from "antd";
import { traceResponseNew, pushDStree } from "../../actions";
import { StoreState } from "../../reducers";
interface TraceGraphColumnProps {
traces: traceResponseNew,
traces: traceResponseNew;
}
interface TableDataSourceItem {
key: string;
operationName: string;
startTime: number;
duration: number;
}
const _TraceGraphColumn = (props: TraceGraphColumnProps) => {
const columns: any = [
{
title: 'Start Time (UTC Time)',
dataIndex: 'startTime',
key: 'startTime',
sorter: (a:any, b:any) => a.startTime - b.startTime,
sortDirections: ['descend', 'ascend'],
render: (value: number) => (new Date(Math.round(value/1000))).toUTCString()
},
{
title: 'Duration (in ms)',
dataIndex: 'duration',
key: 'duration',
sorter: (a:any, b:any) => a.duration - b.duration,
sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'Operation',
dataIndex: 'operationName',
key: 'operationName',
},
];
let dataSource :TableDataSourceItem[] = [];
if (props.traces[0].events.length > 0) {
props.traces[0].events.map((item: (number|string|string[]|pushDStree[])[], index ) => {
if (typeof item[0] === 'number' && typeof item[4] === 'string' && typeof item[6] === 'string' && typeof item[1] === 'string' && typeof item[2] === 'string' )
dataSource.push({startTime: item[0], operationName: item[4] , duration:parseInt(item[6]), key:index.toString()});
});
}
return (
<div>
<Table dataSource={dataSource} columns={columns} size="middle"/>;
</div>
);
key: string;
operationName: string;
startTime: number;
duration: number;
}
const mapStateToProps = (state: StoreState): { traces: traceResponseNew } => {
return { traces : state.traces };
};
const _TraceGraphColumn = (props: TraceGraphColumnProps) => {
const columns: any = [
{
title: "Start Time (UTC Time)",
dataIndex: "startTime",
key: "startTime",
sorter: (a: any, b: any) => a.startTime - b.startTime,
sortDirections: ["descend", "ascend"],
render: (value: number) => new Date(Math.round(value / 1000)).toUTCString(),
},
{
title: "Duration (in ms)",
dataIndex: "duration",
key: "duration",
sorter: (a: any, b: any) => a.duration - b.duration,
sortDirections: ["descend", "ascend"],
render: (value: number) => (value / 1000000).toFixed(2),
},
{
title: "Operation",
dataIndex: "operationName",
key: "operationName",
},
];
let dataSource: TableDataSourceItem[] = [];
if (props.traces[0].events.length > 0) {
props.traces[0].events.map(
(item: (number | string | string[] | pushDStree[])[], index) => {
if (
typeof item[0] === "number" &&
typeof item[4] === "string" &&
typeof item[6] === "string" &&
typeof item[1] === "string" &&
typeof item[2] === "string"
)
dataSource.push({
startTime: item[0],
operationName: item[4],
duration: parseInt(item[6]),
key: index.toString(),
});
},
);
}
return (
<div>
<Table dataSource={dataSource} columns={columns} size="middle" />;
</div>
);
};
const mapStateToProps = (state: StoreState): { traces: traceResponseNew } => {
return { traces: state.traces };
};
export const TraceGraphColumn = connect(mapStateToProps)(_TraceGraphColumn);

View File

@ -1,4 +1,4 @@
export { TraceGraph as default } from './TraceGraph';
export { TraceGraph as default } from "./TraceGraph";
// PNOTE
// Because react.lazy doesn't work on named components

View File

@ -1,116 +1,127 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { Table } from 'antd'
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { NavLink } from "react-router-dom";
import { Table } from "antd";
import { traceResponseNew, fetchTraces, pushDStree } from '../../actions';
import { StoreState } from '../../reducers'
import { traceResponseNew, fetchTraces, pushDStree } from "../../actions";
import { StoreState } from "../../reducers";
interface TraceListProps {
traces: traceResponseNew,
fetchTraces: Function,
traces: traceResponseNew;
fetchTraces: Function;
}
interface TableDataSourceItem {
key: string;
spanid: string;
traceid: string;
operationName: string;
startTime: number;
duration: number;
key: string;
spanid: string;
traceid: string;
operationName: string;
startTime: number;
duration: number;
}
const _TraceList = (props: TraceListProps) => {
// PNOTE (TO DO) - Currently this use of useEffect gives warning. May need to memoise fetchtraces - https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook
const _TraceList = (props: TraceListProps) => {
useEffect(() => {
props.fetchTraces();
}, []);
// PNOTE (TO DO) - Currently this use of useEffect gives warning. May need to memoise fetchtraces - https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook
// PNOTE - code snippet -
// renderList(): JSX.Element[] {
// return this.props.todos.map((todo: Todo) => {
// return (
// <div onClick={() => this.onTodoClick(todo.id)} key={todo.id}>
// {todo.title}
// </div>
// );
// });
// }
useEffect( () => {
props.fetchTraces();
}, []);
const columns: any = [
{
title: "Start Time (UTC Time)",
dataIndex: "startTime",
key: "startTime",
sorter: (a: any, b: any) => a.startTime - b.startTime,
sortDirections: ["descend", "ascend"],
render: (value: number) => new Date(Math.round(value)).toUTCString(),
// PNOTE - code snippet -
// renderList(): JSX.Element[] {
// return this.props.todos.map((todo: Todo) => {
// return (
// <div onClick={() => this.onTodoClick(todo.id)} key={todo.id}>
// {todo.title}
// </div>
// );
// });
// }
// new Date() assumes input in milliseconds. Start Time stamp returned by druid api for span list is in ms
},
{
title: "Duration (in ms)",
dataIndex: "duration",
key: "duration",
sorter: (a: any, b: any) => a.duration - b.duration,
sortDirections: ["descend", "ascend"],
render: (value: number) => (value / 1000000).toFixed(2),
},
{
title: "Operation",
dataIndex: "operationName",
key: "operationName",
},
{
title: "TraceID",
dataIndex: "traceid",
key: "traceid",
render: (text: string) => (
<NavLink to={"/traces/" + text}>{text.slice(-16)}</NavLink>
),
//only last 16 chars have traceID, druid makes it 32 by adding zeros
},
];
const columns: any = [
{
title: 'Start Time (UTC Time)',
dataIndex: 'startTime',
key: 'startTime',
sorter: (a:any, b:any) => a.startTime - b.startTime,
sortDirections: ['descend', 'ascend'],
render: (value: number) => (new Date(Math.round(value))).toUTCString()
let dataSource: TableDataSourceItem[] = [];
// new Date() assumes input in milliseconds. Start Time stamp returned by druid api for span list is in ms
const renderTraces = () => {
if (
typeof props.traces[0] !== "undefined" &&
props.traces[0].events.length > 0
) {
//PNOTE - Template literal should be wrapped in curly braces for it to be evaluated
},
{
title: 'Duration (in ms)',
dataIndex: 'duration',
key: 'duration',
sorter: (a:any, b:any) => a.duration - b.duration,
sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'Operation',
dataIndex: 'operationName',
key: 'operationName',
},
{
title: 'TraceID',
dataIndex: 'traceid',
key: 'traceid',
render: (text :string) => <NavLink to={'/traces/' + text}>{text.slice(-16)}</NavLink>,
//only last 16 chars have traceID, druid makes it 32 by adding zeros
},
];
props.traces[0].events.map(
(item: (number | string | string[] | pushDStree[])[], index) => {
if (
typeof item[0] === "number" &&
typeof item[4] === "string" &&
typeof item[6] === "string" &&
typeof item[1] === "string" &&
typeof item[2] === "string"
)
dataSource.push({
startTime: item[0],
operationName: item[4],
duration: parseInt(item[6]),
spanid: item[1],
traceid: item[2],
key: index.toString(),
});
},
);
//antd table in typescript - https://codesandbox.io/s/react-typescript-669cv
let dataSource :TableDataSourceItem[] = [];
return <Table dataSource={dataSource} columns={columns} size="middle" />;
} else {
return <div> No spans found for given filter!</div>;
}
}; // end of renderTraces
const renderTraces = () => {
if (typeof props.traces[0]!== 'undefined' && props.traces[0].events.length > 0) {
//PNOTE - Template literal should be wrapped in curly braces for it to be evaluated
props.traces[0].events.map((item: (number|string|string[]|pushDStree[])[], index ) => {
if (typeof item[0] === 'number' && typeof item[4] === 'string' && typeof item[6] === 'string' && typeof item[1] === 'string' && typeof item[2] === 'string' )
dataSource.push({startTime: item[0], operationName: item[4] , duration:parseInt(item[6]), spanid:item[1], traceid:item[2], key:index.toString()});
});
//antd table in typescript - https://codesandbox.io/s/react-typescript-669cv
return <Table dataSource={dataSource} columns={columns} size="middle"/>;
} else
{
return <div> No spans found for given filter!</div>
}
};// end of renderTraces
return(
<div>
<div>List of traces with spanID</div>
<div>{renderTraces()}</div>
</div>
)
}
return (
<div>
<div>List of traces with spanID</div>
<div>{renderTraces()}</div>
</div>
);
};
const mapStateToProps = (state: StoreState): { traces: traceResponseNew } => {
return { traces : state.traces };
return { traces: state.traces };
};
export const TraceList = connect(mapStateToProps, {
fetchTraces: fetchTraces,
fetchTraces: fetchTraces,
})(_TraceList);

View File

@ -1,80 +1,78 @@
import React, {useEffect} from 'react';
import { Bar } from 'react-chartjs-2'
import { Card } from 'antd'
import { connect } from 'react-redux';
import React, { useEffect } from "react";
import { Bar } from "react-chartjs-2";
import { Card } from "antd";
import { connect } from "react-redux";
import { getUsageData, GlobalTime, usageDataItem } from '../../actions';
import { StoreState } from '../../reducers'
import { getUsageData, GlobalTime, usageDataItem } from "../../actions";
import { StoreState } from "../../reducers";
interface UsageExplorerProps {
usageData: usageDataItem[],
getUsageData: Function,
globalTime: GlobalTime,
usageData: usageDataItem[];
getUsageData: Function;
globalTime: GlobalTime;
}
const _UsageExplorer = (props: UsageExplorerProps) => {
useEffect(() => {
props.getUsageData(props.globalTime);
}, [props.globalTime]);
useEffect( () => {
props.getUsageData(props.globalTime);
}, [props.globalTime]);
const data = {
labels: props.usageData.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
label: "Span Count",
data: props.usageData.map((s) => s.count),
backgroundColor: "rgba(255, 99, 132, 0.2)",
borderColor: "rgba(255, 99, 132, 1)",
borderWidth: 2,
},
],
};
const data = {
labels: props.usageData.map(s => new Date(s.timestamp/1000000)),
datasets: [
{
label: 'Span Count',
data: props.usageData.map(s => s.count),
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 2,
},
],
}
const options = {
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
fontSize: 10,
},
},
],
xAxes: [
{
type: "time",
// distribution: 'linear', // Bar graph doesn't take lineardistribution type?
const options = {
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
fontSize: 10,
},
},
],
xAxes: [
{
type: 'time',
// distribution: 'linear', // Bar graph doesn't take lineardistribution type?
ticks: {
beginAtZero: true,
fontSize: 10,
},
},
],
},
legend: {
display: false,
},
};
ticks: {
beginAtZero: true,
fontSize: 10,
},
},
],
},
legend: {
display: false,
}
}
return (
<React.Fragment>
{/* PNOTE - TODO - Keep it in reponsive row column tab */}
<Card style={{ width: "50%", margin: 20 }} bodyStyle={{ padding: 20 }}>
<Bar data={data} options={options} />
</Card>
</React.Fragment>
);
};
return(
<React.Fragment>
{/* PNOTE - TODO - Keep it in reponsive row column tab */}
<Card style={{ width: "50%" , margin:20 }} bodyStyle={{padding:20 }}>
<Bar data={data} options={options} />
</Card>
</React.Fragment>
);
}
const mapStateToProps = (state: StoreState): { usageData: usageDataItem[], globalTime: GlobalTime } => {
return { usageData : state.usageDate, globalTime: state.globalTime };
const mapStateToProps = (
state: StoreState,
): { usageData: usageDataItem[]; globalTime: GlobalTime } => {
return { usageData: state.usageDate, globalTime: state.globalTime };
};
export const UsageExplorer = connect(mapStateToProps, {
getUsageData: getUsageData,
getUsageData: getUsageData,
})(_UsageExplorer);

View File

@ -1 +1 @@
export { UsageExplorer as default } from './UsageExplorer';
export { UsageExplorer as default } from "./UsageExplorer";

View File

@ -0,0 +1,4 @@
export const ENVIRONMENT = {
baseURL: "/api",
// baseURL: "http://104.211.113.204:8080",
};

View File

@ -0,0 +1,3 @@
export enum LOCAL_STORAGE {
METRICS_TIME_IN_DURATION = "metricsTimeDuration",
}

View File

@ -0,0 +1,3 @@
export enum METRICS_PAGE_QUERY_PARAM {
time = "time",
}

View File

@ -1,34 +1,32 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import { ThemeSwitcherProvider } from "react-css-theme-switcher";
import thunk from 'redux-thunk';
import thunk from "redux-thunk";
// import { NavLink, BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import AppWrapper from './components/AppWrapper';
import './assets/index.css';
import { reducers } from './reducers';
import AppWrapper from "./components/AppWrapper";
import "./assets/index.css";
import { reducers } from "./reducers";
// import Signup from './components/Signup';
const store = createStore(reducers, applyMiddleware(thunk))
// @ts-ignore
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
const themes = {
dark: `${process.env.PUBLIC_URL}/dark-theme.css`,
light: `${process.env.PUBLIC_URL}/light-theme.css`,
dark: `${process.env.PUBLIC_URL}/dark-theme.css`,
light: `${process.env.PUBLIC_URL}/light-theme.css`,
};
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<ThemeSwitcherProvider themeMap={themes} defaultTheme="dark">
<AppWrapper />
{/* <App /> */}
</ThemeSwitcherProvider>
</React.StrictMode>
</Provider>,
document.querySelector('#root')
<Provider store={store}>
<React.StrictMode>
<ThemeSwitcherProvider themeMap={themes} defaultTheme="dark">
<AppWrapper />
{/* <App /> */}
</ThemeSwitcherProvider>
</React.StrictMode>
</Provider>,
document.querySelector("#root"),
);

View File

@ -1,14 +1,17 @@
import { ActionTypes, Action, GlobalTime } from '../actions';
export const updateGlobalTimeReducer = (state:GlobalTime = {maxTime:Date.now()*1000000, minTime:(Date.now()-15*60*1000)*1000000}, action: Action) => {
// Initial global state is time now and 15 minute interval
switch (action.type){
case ActionTypes.updateTimeInterval:
return action.payload;
default:
return state;
}
import { ActionTypes, Action, GlobalTime } from "../actions";
export const updateGlobalTimeReducer = (
state: GlobalTime = {
maxTime: Date.now() * 1000000,
minTime: (Date.now() - 15 * 60 * 1000) * 1000000,
},
action: Action,
) => {
// Initial global state is time now and 15 minute interval
switch (action.type) {
case ActionTypes.updateTimeInterval:
return action.payload;
default:
return state;
}
};

View File

@ -1,34 +1,48 @@
import { combineReducers } from 'redux';
import { traceResponseNew, spansWSameTraceIDResponse, servicesListItem, metricItem, topEndpointListItem, usageDataItem, GlobalTime, customMetricsItem, TraceFilters } from '../actions';
import { updateGlobalTimeReducer } from './global';
import { filteredTraceMetricsReducer, serviceMetricsReducer, serviceTableReducer, topEndpointsReducer } from './metrics';
import { traceFiltersReducer, inputsReducer} from './traceFilters'
import { traceItemReducer, tracesReducer } from './traces'
import { usageDataReducer } from './usage'
import { combineReducers } from "redux";
import {
traceResponseNew,
spansWSameTraceIDResponse,
servicesListItem,
metricItem,
topEndpointListItem,
usageDataItem,
GlobalTime,
customMetricsItem,
TraceFilters,
} from "../actions";
import { updateGlobalTimeReducer } from "./global";
import {
filteredTraceMetricsReducer,
serviceMetricsReducer,
serviceTableReducer,
topEndpointsReducer,
} from "./metrics";
import { traceFiltersReducer, inputsReducer } from "./traceFilters";
import { traceItemReducer, tracesReducer } from "./traces";
import { usageDataReducer } from "./usage";
export interface StoreState{
traceFilters: TraceFilters,
inputTag: string,
traces: traceResponseNew,
traceItem: spansWSameTraceIDResponse ,
servicesList: servicesListItem[],
serviceMetrics:metricItem[],
topEndpointsList:topEndpointListItem[],
usageDate:usageDataItem[],
globalTime:GlobalTime,
filteredTraceMetrics:customMetricsItem[],
export interface StoreState {
traceFilters: TraceFilters;
inputTag: string;
traces: traceResponseNew;
traceItem: spansWSameTraceIDResponse;
servicesList: servicesListItem[];
serviceMetrics: metricItem[];
topEndpointsList: topEndpointListItem[];
usageDate: usageDataItem[];
globalTime: GlobalTime;
filteredTraceMetrics: customMetricsItem[];
}
export const reducers = combineReducers<StoreState>({
traceFilters: traceFiltersReducer,
inputTag: inputsReducer,
traces: tracesReducer,
traceItem: traceItemReducer,
servicesList: serviceTableReducer,
serviceMetrics: serviceMetricsReducer,
topEndpointsList: topEndpointsReducer,
usageDate: usageDataReducer,
globalTime: updateGlobalTimeReducer,
filteredTraceMetrics:filteredTraceMetricsReducer,
traceFilters: traceFiltersReducer,
inputTag: inputsReducer,
traces: tracesReducer,
traceItem: traceItemReducer,
servicesList: serviceTableReducer,
serviceMetrics: serviceMetricsReducer,
topEndpointsList: topEndpointsReducer,
usageDate: usageDataReducer,
globalTime: updateGlobalTimeReducer,
filteredTraceMetrics: filteredTraceMetricsReducer,
});

View File

@ -1,38 +1,79 @@
import { ActionTypes, Action, servicesListItem, metricItem, topEndpointListItem, customMetricsItem} from '../actions'
import {
ActionTypes,
Action,
servicesListItem,
metricItem,
topEndpointListItem,
customMetricsItem,
} from "../actions";
export const serviceTableReducer = (state: servicesListItem[] = [{"serviceName": '', "p99": 0, "avgDuration": 0, "numCalls": 0, "callRate": 0, "numErrors": 0, "errorRate": 0,}], action: Action) => {
switch (action.type) {
case ActionTypes.getServicesList:
return action.payload;
default:
return state;
}
};
export const serviceTableReducer = (
state: servicesListItem[] = [
{
serviceName: "",
p99: 0,
avgDuration: 0,
numCalls: 0,
callRate: 0,
numErrors: 0,
errorRate: 0,
},
],
action: Action,
) => {
switch (action.type) {
case ActionTypes.getServicesList:
return action.payload;
default:
return state;
}
};
export const serviceMetricsReducer = (state: metricItem[] = [{"timestamp":0,"p50":0,"p90":0,"p99":0,"numCalls":0,"callRate":0.0,"numErrors":0,"errorRate":0}], action: Action) => {
switch (action.type) {
case ActionTypes.getServiceMetrics:
return action.payload;
default:
return state;
}
};
export const serviceMetricsReducer = (
state: metricItem[] = [
{
timestamp: 0,
p50: 0,
p90: 0,
p99: 0,
numCalls: 0,
callRate: 0.0,
numErrors: 0,
errorRate: 0,
},
],
action: Action,
) => {
switch (action.type) {
case ActionTypes.getServiceMetrics:
return action.payload;
default:
return state;
}
};
export const topEndpointsReducer = (state: topEndpointListItem[] = [{"p50":0,"p90":0,"p99":0,"numCalls":0,"name":''}], action: Action) => {
switch (action.type) {
case ActionTypes.getTopEndpoints:
return action.payload;
default:
return state;
}
};
export const topEndpointsReducer = (
state: topEndpointListItem[] = [
{ p50: 0, p90: 0, p99: 0, numCalls: 0, name: "" },
],
action: Action,
) => {
switch (action.type) {
case ActionTypes.getTopEndpoints:
return action.payload;
default:
return state;
}
};
export const filteredTraceMetricsReducer = (state: customMetricsItem[] = [{"timestamp": 0, "value": 0}], action: Action) => {
switch (action.type) {
case ActionTypes.getFilteredTraceMetrics:
return action.payload;
default:
return state;
}
};
export const filteredTraceMetricsReducer = (
state: customMetricsItem[] = [{ timestamp: 0, value: 0 }],
action: Action,
) => {
switch (action.type) {
case ActionTypes.getFilteredTraceMetrics:
return action.payload;
default:
return state;
}
};

View File

@ -1,26 +1,35 @@
import { ActionTypes, TraceFilters, updateInputTagAction, updateTraceFiltersAction } from '../actions';
export const traceFiltersReducer = (state:TraceFilters = {'service':'', 'tags':[],'operation':'','latency':{'min':'','max':''}}, action: updateTraceFiltersAction) => {
switch (action.type){
case ActionTypes.updateTraceFilters:
return action.payload;
default:
return state;
}
import {
ActionTypes,
TraceFilters,
updateInputTagAction,
updateTraceFiltersAction,
} from "../actions";
export const traceFiltersReducer = (
state: TraceFilters = {
service: "",
tags: [],
operation: "",
latency: { min: "", max: "" },
},
action: updateTraceFiltersAction,
) => {
switch (action.type) {
case ActionTypes.updateTraceFilters:
return action.payload;
default:
return state;
}
};
export const inputsReducer = (state:string = '', action:updateInputTagAction) => {
switch (action.type){
case ActionTypes.updateInput:
return action.payload;
default:
return state;
}
}
export const inputsReducer = (
state: string = "",
action: updateInputTagAction,
) => {
switch (action.type) {
case ActionTypes.updateInput:
return action.payload;
default:
return state;
}
};

View File

@ -1,21 +1,33 @@
import { ActionTypes, Action, traceResponseNew, spanList, spansWSameTraceIDResponse } from '../actions';
import {
ActionTypes,
Action,
traceResponseNew,
spanList,
spansWSameTraceIDResponse,
} from "../actions";
// PNOTE - Initializing is a must for state variable otherwise it gives an error in reducer
var spanlistinstance :spanList ={ events: [], segmentID: '', columns: []} ;
export const tracesReducer = (state: traceResponseNew = {"0": spanlistinstance} , action: Action) => {
switch (action.type) {
case ActionTypes.fetchTraces:
return action.payload;
default:
return state;
}
};
var spanlistinstance: spanList = { events: [], segmentID: "", columns: [] };
export const tracesReducer = (
state: traceResponseNew = { "0": spanlistinstance },
action: Action,
) => {
switch (action.type) {
case ActionTypes.fetchTraces:
return action.payload;
default:
return state;
}
};
export const traceItemReducer = (state: spansWSameTraceIDResponse = {"0": spanlistinstance}, action: Action) => {
switch (action.type) {
case ActionTypes.fetchTraceItem:
return action.payload;
default:
return state;
}
};
export const traceItemReducer = (
state: spansWSameTraceIDResponse = { "0": spanlistinstance },
action: Action,
) => {
switch (action.type) {
case ActionTypes.fetchTraceItem:
return action.payload;
default:
return state;
}
};

View File

@ -1,10 +1,13 @@
import { ActionTypes, Action, usageDataItem, } from '../actions'
import { ActionTypes, Action, usageDataItem } from "../actions";
export const usageDataReducer = (state: usageDataItem[] = [{"timestamp": 0, "count": 0, }], action: Action) => {
switch (action.type) {
case ActionTypes.getUsageData:
return action.payload;
default:
return state;
}
};
export const usageDataReducer = (
state: usageDataItem[] = [{ timestamp: 0, count: 0 }],
action: Action,
) => {
switch (action.type) {
case ActionTypes.getUsageData:
return action.payload;
default:
return state;
}
};

View File

@ -1,8 +1,8 @@
// dark-theme.less
@import '~antd/lib/style/color/colorPalette.less';
@import '~antd/dist/antd.less';
@import '~antd/lib/style/themes/dark.less';
@import "~antd/lib/style/color/colorPalette.less";
@import "~antd/dist/antd.less";
@import "~antd/lib/style/themes/dark.less";
// @primary-color: #00adb5;
// @border-radius-base: 4px;

View File

@ -1,9 +1,9 @@
/* light-theme.less */
/* light-theme.less */
@import '~antd/lib/style/color/colorPalette.less';
@import '~antd/dist/antd.less';
@import '~antd/lib/style/themes/default.less';
@import "~antd/lib/style/color/colorPalette.less";
@import "~antd/dist/antd.less";
@import "~antd/lib/style/themes/default.less";
/* These are shared variables that can be extracted to their own file */
/* These are shared variables that can be extracted to their own file */
@primary-color: #00adb5;
@border-radius-base: 4px;

View File

@ -1 +1 @@
declare module 'd3-tip';
declare module "d3-tip";

View File

@ -1,39 +1,39 @@
//You must first install the vis and react types 'npm install --save-dev @types/vis @types/react'
declare module "react-graph-vis" {
import { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis";
import { Component } from "react";
import { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis";
import { Component } from "react";
export { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis";
export { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis";
export interface graphEvents {
[event: NetworkEvents]: (params?: any) => void;
}
export interface graphEvents {
[event: NetworkEvents]: (params?: any) => void;
}
//Doesn't appear that this module supports passing in a vis.DataSet directly. Once it does graph can just use the Data object from vis.
export interface graphData {
nodes: Node[];
edges: Edge[];
}
//Doesn't appear that this module supports passing in a vis.DataSet directly. Once it does graph can just use the Data object from vis.
export interface graphData {
nodes: Node[];
edges: Edge[];
}
export interface NetworkGraphProps {
graph: graphData;
options?: Options;
events?: graphEvents;
getNetwork?: (network: Network) => void;
identifier?: string;
style?: React.CSSProperties;
getNodes?: (nodes: DataSet) => void;
getEdges?: (edges: DataSet) => void;
}
export interface NetworkGraphProps {
graph: graphData;
options?: Options;
events?: graphEvents;
getNetwork?: (network: Network) => void;
identifier?: string;
style?: React.CSSProperties;
getNodes?: (nodes: DataSet) => void;
getEdges?: (edges: DataSet) => void;
}
export interface NetworkGraphState {
identifier: string;
}
export interface NetworkGraphState {
identifier: string;
}
export default class NetworkGraph extends Component<
NetworkGraphProps,
NetworkGraphState
> {
render();
}
}
export default class NetworkGraph extends Component<
NetworkGraphProps,
NetworkGraphState
> {
render();
}
}

View File

@ -1,2 +1,2 @@
import { createBrowserHistory } from 'history';
import { createBrowserHistory } from "history";
export default createBrowserHistory();

View File

@ -1,106 +1,100 @@
import { pushDStree, span, RefItem } from '../actions';
import { pushDStree, span, RefItem } from "../actions";
// PNOTE - should the data be taken from redux or only through props? - Directly as arguments
export const spanToTreeUtil = (spanlist: span[]): pushDStree => {
// Initializing tree. What should be returned is trace is empty? We should have better error handling
let tree: pushDStree = {
id: "empty",
name: "default",
value: 0,
time: 0,
startTime: 0,
tags: [],
children: [],
};
// let spans :spanItem[]= trace.spans;
if (spanlist) {
// Create a dict with spanIDs as keys
// PNOTE
// Can we now assign different strings as id - Yes
// https://stackoverflow.com/questions/15877362/declare-and-initialize-a-dictionary-in-typescript
export const spanToTreeUtil = (spanlist :span[]) :pushDStree => {
let mapped_array: { [id: string]: span } = {};
// Initializing tree. What should be returned is trace is empty? We should have better error handling
let tree :pushDStree ={id:'empty', name:'default', value:0, time: 0, startTime:0, tags:[], children:[]};
for (let i = 0; i < spanlist.length; i++) {
mapped_array[spanlist[i][1]] = spanlist[i];
mapped_array[spanlist[i][1]][10] = [];
}
// let spans :spanItem[]= trace.spans;
for (let id in mapped_array) {
let child_span = mapped_array[id];
if(spanlist){
//mapping tags to new structure
let tags_temp = [];
if (child_span[7] !== null && child_span[8] !== null) {
if (
typeof child_span[7] === "string" &&
typeof child_span[8] === "string"
) {
tags_temp.push({ key: child_span[7], value: child_span[8] });
} else if (child_span[7].length > 0 && child_span[8].length > 0) {
for (let j = 0; j < child_span[7].length; j++) {
tags_temp.push({ key: child_span[7][j], value: child_span[8][j] });
}
}
}
// Create a dict with spanIDs as keys
// PNOTE
// Can we now assign different strings as id - Yes
// https://stackoverflow.com/questions/15877362/declare-and-initialize-a-dictionary-in-typescript
let push_object: pushDStree = {
id: child_span[1],
name: child_span[3] + ": " + child_span[4],
value: parseInt(child_span[6]),
time: parseInt(child_span[6]),
startTime: child_span[0],
tags: tags_temp,
children: mapped_array[id][10],
};
let referencesArr = mapped_array[id][9];
let refArray = [];
if (typeof referencesArr === "string") {
refArray.push(referencesArr);
} else {
refArray = referencesArr;
}
let references: RefItem[] = [];
refArray.forEach((element) => {
element = element
.replaceAll("{", "")
.replaceAll("}", "")
.replaceAll(" ", "");
let arr = element.split(",");
let refItem = { traceID: "", spanID: "", refType: "" };
arr.forEach((obj) => {
let arr2 = obj.split("=");
if (arr2[0] === "TraceId") {
refItem["traceID"] = arr2[1];
} else if (arr2[0] === "SpanId") {
refItem["spanID"] = arr2[1];
} else if (arr2[0] === "RefType") {
refItem["refType"] = arr2[1];
}
});
let mapped_array : {[id: string] : span;} = {};
references.push(refItem);
});
for(let i=0; i<spanlist.length; i++){
mapped_array[spanlist[i][1]] = spanlist[i];
mapped_array[spanlist[i][1]][10] = [];
}
if (references.length !== 0 && references[0].spanID.length !== 0) {
if (references[0].refType === "CHILD_OF") {
let parentID = references[0].spanID;
mapped_array[parentID][10].push(push_object);
}
} else {
tree = push_object;
}
} // end of for loop
} // end of if(spans)
for(let id in mapped_array){
let child_span = mapped_array[id];
//mapping tags to new structure
let tags_temp = [];
if (child_span[7]!==null && child_span[8]!==null )
{
if (typeof(child_span[7])==='string' && typeof(child_span[8])==='string')
{
tags_temp.push({key:child_span[7],value:child_span[8]})
} else if (child_span[7].length>0 && child_span[8].length>0)
{
for(let j=0;j<child_span[7].length; j++)
{
tags_temp.push({key:child_span[7][j],value:child_span[8][j]})
}
}
}
let push_object :pushDStree = {
id: child_span[1],
name: child_span[3] + ": " + child_span[4],
value: parseInt(child_span[6]),
time: parseInt(child_span[6]),
startTime: child_span[0],
tags:tags_temp,
children: mapped_array[id][10]
}
let referencesArr = mapped_array[id][9]
let refArray = []
if(typeof(referencesArr) === "string"){
refArray.push(referencesArr)
}else{
refArray = referencesArr
}
let references :RefItem[] = [];
refArray.forEach(element => {
element = element.replaceAll("{", "").replaceAll("}", "").replaceAll(" ", "")
let arr = element.split(",")
let refItem = {"traceID": "", "spanID": "", "refType": ""};
arr.forEach(obj => {
let arr2 = obj.split("=")
if(arr2[0]==="TraceId"){
refItem["traceID"] = arr2[1];
}else if(arr2[0]==="SpanId"){
refItem["spanID"] = arr2[1];
}else if(arr2[0]==="RefType"){
refItem["refType"] = arr2[1];
}
});
references.push(refItem);
});
if(references.length !== 0 && references[0].spanID.length !== 0){
if(references[0].refType === "CHILD_OF"){
let parentID = references[0].spanID;
mapped_array[parentID][10].push(push_object);
}
}
else{
tree = push_object;
}
}// end of for loop
}// end of if(spans)
return(tree)
}
return tree;
};

View File

@ -1,28 +1,22 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "esnext",
"target": "es5",
"jsx": "react",
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": [
"src"
]
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "esnext",
"target": "es5",
"jsx": "react",
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}

View File

@ -2672,7 +2672,7 @@ ansi-cyan@^0.1.1:
dependencies:
ansi-wrap "0.1.0"
ansi-escapes@^4.2.1, ansi-escapes@^4.3.1:
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==
@ -3036,6 +3036,11 @@ astral-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
async-done@^1.2.0, async-done@^1.2.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2"
@ -3958,6 +3963,14 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
dependencies:
slice-ansi "^3.0.0"
string-width "^4.2.0"
cli-width@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
@ -4145,6 +4158,11 @@ commander@^4.1.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
common-tags@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
@ -4155,6 +4173,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
component-emitter@^1.2.1, component-emitter@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@ -5179,6 +5202,13 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "2.1.2"
debug@^4.2.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@ -5627,7 +5657,7 @@ enhanced-resolve@^4.3.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
enquirer@^2.3.5:
enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@ -6067,7 +6097,7 @@ execa@^1.0.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^4.0.0:
execa@^4.0.0, execa@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
@ -6302,7 +6332,7 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
figures@^3.0.0:
figures@^3.0.0, figures@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
@ -6412,6 +6442,21 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
find-versions@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965"
integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==
dependencies:
semver-regex "^3.1.2"
findup-sync@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
@ -7250,6 +7295,22 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
husky@4.3.8:
version "4.3.8"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d"
integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==
dependencies:
chalk "^4.0.0"
ci-info "^2.0.0"
compare-versions "^3.6.0"
cosmiconfig "^7.0.0"
find-versions "^4.0.0"
opencollective-postinstall "^2.0.2"
pkg-dir "^5.0.0"
please-upgrade-node "^3.2.0"
slash "^3.0.0"
which-pm-runs "^1.0.0"
hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
@ -8771,6 +8832,41 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
lint-staged@10.5.3:
version "10.5.3"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.3.tgz#c682838b3eadd4c864d1022da05daa0912fb1da5"
integrity sha512-TanwFfuqUBLufxCc3RUtFEkFraSPNR3WzWcGF39R3f2J7S9+iF9W0KTVLfSy09lYGmZS5NDCxjNvhGMSJyFCWg==
dependencies:
chalk "^4.1.0"
cli-truncate "^2.1.0"
commander "^6.2.0"
cosmiconfig "^7.0.0"
debug "^4.2.0"
dedent "^0.7.0"
enquirer "^2.3.6"
execa "^4.1.0"
listr2 "^3.2.2"
log-symbols "^4.0.0"
micromatch "^4.0.2"
normalize-path "^3.0.0"
please-upgrade-node "^3.2.0"
string-argv "0.3.1"
stringify-object "^3.3.0"
listr2@^3.2.2:
version "3.2.3"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.2.3.tgz#ef9e0d790862f038dde8a9837be552b1adfd1c07"
integrity sha512-vUb80S2dSUi8YxXahO8/I/s29GqnOL8ozgHVLjfWQXa03BNEeS1TpBLjh2ruaqq5ufx46BRGvfymdBSuoXET5w==
dependencies:
chalk "^4.1.0"
cli-truncate "^2.1.0"
figures "^3.2.0"
indent-string "^4.0.0"
log-update "^4.0.0"
p-map "^4.0.0"
rxjs "^6.6.3"
through "^2.3.8"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -8847,6 +8943,13 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@ -8927,6 +9030,23 @@ lodash.uniq@^4.3.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
log-symbols@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
dependencies:
chalk "^4.0.0"
log-update@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
dependencies:
ansi-escapes "^4.3.0"
cli-cursor "^3.1.0"
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
loglevel@^1.6.8:
version "1.7.0"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0"
@ -9756,6 +9876,11 @@ open@^7.0.2:
is-docker "^2.0.0"
is-wsl "^2.1.1"
opencollective-postinstall@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
opn@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
@ -9883,6 +10008,13 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
p-map@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
@ -10192,6 +10324,13 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
pkg-dir@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760"
integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==
dependencies:
find-up "^5.0.0"
pkg-up@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
@ -10199,6 +10338,13 @@ pkg-up@3.1.0:
dependencies:
find-up "^3.0.0"
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
dependencies:
semver-compare "^1.0.0"
plugin-error@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace"
@ -10940,6 +11086,11 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
pretty-bytes@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b"
@ -12496,7 +12647,7 @@ rw@1:
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=
rxjs@^6.6.0:
rxjs@^6.6.0, rxjs@^6.6.3:
version "6.6.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
@ -12627,6 +12778,11 @@ selfsigned@^1.10.7:
dependencies:
node-forge "^0.10.0"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
semver-greatest-satisfied-range@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b"
@ -12634,6 +12790,11 @@ semver-greatest-satisfied-range@^1.1.0:
dependencies:
sver-compat "^1.5.0"
semver-regex@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807"
integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -12838,6 +12999,24 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
dependencies:
ansi-styles "^4.0.0"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
dependencies:
ansi-styles "^4.0.0"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -13130,6 +13309,11 @@ strict-uri-encode@^1.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
string-argv@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
string-convert@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
@ -13530,7 +13714,7 @@ through2@^2.0.0, through2@^2.0.3, through2@~2.0.0:
readable-stream "~2.3.6"
xtend "~4.0.1"
through@^2.3.6:
through@^2.3.6, through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@ -14444,6 +14628,11 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-pm-runs@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@^1.2.14, which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"