From 030592bf5fe131c12a4fbe56f3e3f8a2088b4aaf Mon Sep 17 00:00:00 2001
From: manorlh <44364426+manorlh@users.noreply.github.com>
Date: Thu, 16 Apr 2020 21:28:37 +0300
Subject: [PATCH] Compare (#290)
* feat(graphs): compare multiple reports graph
---
manor.js | 0
src/configManager/helpers/validators.js | 2 +-
ui/package-lock.json | 107 ++++---
ui/package.json | 2 +-
ui/src/components/Checkbox/Checkbox.js | 51 +++
ui/src/components/Checkbox/Checkbox.scss | 57 ++++
ui/src/components/Checkbox/style/_colors.scss | 4 +
ui/src/components/ReactTable/ReactTable.js | 2 +-
.../ReactTable/style/table-style.scss | 2 +-
ui/src/features/components/Box/style.scss | 8 +-
ui/src/features/components/Report/Charts.js | 169 ++++++++++
.../components/Report/compareReports.js | 301 ++++++++++++++++++
ui/src/features/components/Report/index.js | 139 +++-----
.../TestForm/collapsibleScenarioConfig.js | 12 +-
.../components/TestForm/dragableWrapper.js | 112 +++++++
ui/src/features/components/TestForm/index.js | 31 +-
.../features/components/TestForm/stepsList.js | 10 +-
ui/src/features/configurationColumn.js | 173 ++++++----
ui/src/features/configurationColumn.scss | 18 +-
ui/src/features/get-last-reports.js | 52 ++-
ui/src/features/get-test-reports.js | 44 ++-
ui/src/features/redux/action.js | 6 +-
.../features/redux/actions/reportsActions.js | 44 +--
.../features/redux/reducers/reportsReducer.js | 15 +-
ui/src/features/redux/saga/reportsSagas.js | 19 +-
.../redux/selectors/reportsSelector.js | 131 +++++---
ui/src/features/redux/types/reportsTypes.js | 10 +-
27 files changed, 1190 insertions(+), 331 deletions(-)
create mode 100644 manor.js
create mode 100644 ui/src/components/Checkbox/Checkbox.js
create mode 100644 ui/src/components/Checkbox/Checkbox.scss
create mode 100644 ui/src/components/Checkbox/style/_colors.scss
create mode 100644 ui/src/features/components/Report/Charts.js
create mode 100644 ui/src/features/components/Report/compareReports.js
create mode 100644 ui/src/features/components/TestForm/dragableWrapper.js
diff --git a/manor.js b/manor.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/configManager/helpers/validators.js b/src/configManager/helpers/validators.js
index a82ba8d87..d2936550a 100644
--- a/src/configManager/helpers/validators.js
+++ b/src/configManager/helpers/validators.js
@@ -15,4 +15,4 @@ module.exports.validateBenchmarkWeights = (req, res, next) => {
}
}
return next();
-};
\ No newline at end of file
+};
diff --git a/ui/package-lock.json b/ui/package-lock.json
index ebe69a78f..5283034ea 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -3107,7 +3107,7 @@
},
"balanced-match": {
"version": "0.4.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
+ "resolved": "http://npm.zooz.co:8083/balanced-match/-/balanced-match-0.4.2.tgz",
"integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg="
},
"base": {
@@ -4203,40 +4203,40 @@
},
"d3-array": {
"version": "1.2.4",
- "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
+ "resolved": "http://npm.zooz.co:8083/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
},
"d3-collection": {
"version": "1.0.7",
- "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
+ "resolved": "http://npm.zooz.co:8083/d3-collection/-/d3-collection-1.0.7.tgz",
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
},
"d3-color": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz",
- "integrity": "sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw=="
+ "version": "1.4.0",
+ "resolved": "http://npm.zooz.co:8083/d3-color/-/d3-color-1.4.0.tgz",
+ "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg=="
},
"d3-format": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz",
- "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ=="
+ "version": "1.4.4",
+ "resolved": "http://npm.zooz.co:8083/d3-format/-/d3-format-1.4.4.tgz",
+ "integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw=="
},
"d3-interpolate": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
- "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
+ "version": "1.4.0",
+ "resolved": "http://npm.zooz.co:8083/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
+ "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
"requires": {
"d3-color": "1"
}
},
"d3-path": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz",
- "integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA=="
+ "version": "1.0.9",
+ "resolved": "http://npm.zooz.co:8083/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"d3-scale": {
"version": "2.2.2",
- "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
+ "resolved": "http://npm.zooz.co:8083/d3-scale/-/d3-scale-2.2.2.tgz",
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
"requires": {
"d3-array": "^1.2.0",
@@ -4248,22 +4248,22 @@
}
},
"d3-shape": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz",
- "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==",
+ "version": "1.3.7",
+ "resolved": "http://npm.zooz.co:8083/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
- "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
+ "version": "1.1.0",
+ "resolved": "http://npm.zooz.co:8083/d3-time/-/d3-time-1.1.0.tgz",
+ "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
},
"d3-time-format": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz",
- "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==",
+ "version": "2.2.3",
+ "resolved": "http://npm.zooz.co:8083/d3-time-format/-/d3-time-format-2.2.3.tgz",
+ "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==",
"requires": {
"d3-time": "1"
}
@@ -4303,7 +4303,7 @@
},
"decimal.js-light": {
"version": "2.5.0",
- "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.0.tgz",
+ "resolved": "http://npm.zooz.co:8083/decimal.js-light/-/decimal.js-light-2.5.0.tgz",
"integrity": "sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg=="
},
"decode-uri-component": {
@@ -7966,7 +7966,7 @@
},
"lodash.debounce": {
"version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "resolved": "http://npm.zooz.co:8083/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"lodash.merge": {
@@ -8131,9 +8131,9 @@
}
},
"math-expression-evaluator": {
- "version": "1.2.17",
- "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
- "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw="
+ "version": "1.2.22",
+ "resolved": "http://npm.zooz.co:8083/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz",
+ "integrity": "sha512-L0j0tFVZBQQLeEjmWOvDLoRciIY8gQGWahvkztXUal8jH8R5Rlqo9GCvgqvXcy9LQhEWdQCVvzqAbxgYNt4blQ=="
},
"md5.js": {
"version": "1.3.5",
@@ -9682,7 +9682,7 @@
},
"raf": {
"version": "3.4.1",
- "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "resolved": "http://npm.zooz.co:8083/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"requires": {
"performance-now": "^2.1.0"
@@ -10039,9 +10039,9 @@
}
},
"react-smooth": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-1.0.2.tgz",
- "integrity": "sha512-pIGzL1g9VGAsRsdZQokIK0vrCkcdKtnOnS1gyB2rrowdLy69lNSWoIjCTWAfgbiYvria8tm5hEZqj+jwXMkV4A==",
+ "version": "1.0.5",
+ "resolved": "http://npm.zooz.co:8083/react-smooth/-/react-smooth-1.0.5.tgz",
+ "integrity": "sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w==",
"requires": {
"lodash": "~4.17.4",
"prop-types": "^15.6.0",
@@ -10153,27 +10153,27 @@
}
},
"recharts": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-1.6.2.tgz",
- "integrity": "sha512-NqVN8Hq5wrrBthTxQB+iCnZjup1dc+AYRIB6Q9ck9UjdSJTt4PbLepGpudQEYJEN5iIpP/I2vThC4uiTJa7xUQ==",
+ "version": "1.8.5",
+ "resolved": "http://npm.zooz.co:8083/recharts/-/recharts-1.8.5.tgz",
+ "integrity": "sha512-tM9mprJbXVEBxjM7zHsIy6Cc41oO/pVYqyAsOHLxlJrbNBuLs0PHB3iys2M+RqCF0//k8nJtZF6X6swSkWY3tg==",
"requires": {
"classnames": "^2.2.5",
- "core-js": "^2.5.1",
+ "core-js": "^2.6.10",
"d3-interpolate": "^1.3.0",
"d3-scale": "^2.1.0",
"d3-shape": "^1.2.0",
"lodash": "^4.17.5",
"prop-types": "^15.6.0",
"react-resize-detector": "^2.3.0",
- "react-smooth": "^1.0.0",
+ "react-smooth": "^1.0.5",
"recharts-scale": "^0.4.2",
"reduce-css-calc": "^1.3.0"
},
"dependencies": {
"core-js": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
- "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
+ "version": "2.6.11",
+ "resolved": "http://npm.zooz.co:8083/core-js/-/core-js-2.6.11.tgz",
+ "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
},
"react-resize-detector": {
"version": "2.3.0",
@@ -10189,9 +10189,9 @@
}
},
"recharts-scale": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.2.tgz",
- "integrity": "sha512-p/cKt7j17D1CImLgX2f5+6IXLbRHGUQkogIp06VUoci/XkhOQiGSzUrsD1uRmiI7jha4u8XNFOjkHkzzBPivMg==",
+ "version": "0.4.3",
+ "resolved": "http://npm.zooz.co:8083/recharts-scale/-/recharts-scale-0.4.3.tgz",
+ "integrity": "sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA==",
"requires": {
"decimal.js-light": "^2.4.1"
}
@@ -10231,7 +10231,7 @@
},
"reduce-css-calc": {
"version": "1.3.0",
- "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz",
+ "resolved": "http://npm.zooz.co:8083/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz",
"integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=",
"requires": {
"balanced-match": "^0.4.2",
@@ -10240,11 +10240,18 @@
}
},
"reduce-function-call": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz",
- "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=",
+ "version": "1.0.3",
+ "resolved": "http://npm.zooz.co:8083/reduce-function-call/-/reduce-function-call-1.0.3.tgz",
+ "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==",
"requires": {
- "balanced-match": "^0.4.2"
+ "balanced-match": "^1.0.0"
+ },
+ "dependencies": {
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "http://npm.zooz.co:8083/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ }
}
},
"redux": {
diff --git a/ui/package.json b/ui/package.json
index b18a6f044..09416c542 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -101,7 +101,7 @@
"react-router-redux": "^5.0.0-alpha.9",
"react-table": "^6.8.6",
"react-tooltip": "^3.9.0",
- "recharts": "^1.5.0",
+ "recharts": "^1.8.5",
"redux": "^4.0.0",
"redux-saga": "^0.16.0",
"reselect": "^4.0.0",
diff --git a/ui/src/components/Checkbox/Checkbox.js b/ui/src/components/Checkbox/Checkbox.js
new file mode 100644
index 000000000..b6c64b058
--- /dev/null
+++ b/ui/src/components/Checkbox/Checkbox.js
@@ -0,0 +1,51 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import style from './Checkbox.scss'
+import FontAwesome from '../FontAwesome/FontAwesome.export'
+import classnames from 'classnames'
+
+class Checkbox extends Component {
+ render () {
+ const { disabled, checked, indeterminate } = this.props
+ const icon = this.getIcon()
+ return (
+
+ {icon && }
+
+
+ )
+ }
+
+ handleOnChange = (e) => {
+ e.stopPropagation()
+ const { onChange, disabled, checked } = this.props
+ if (onChange && !disabled) {
+ onChange(!checked)
+ }
+ }
+
+ getIcon = () => {
+ const { indeterminate, checked } = this.props
+ if (indeterminate) {
+ return 'minus'
+ }
+ return checked && 'check'
+ }
+}
+
+Checkbox.propTypes = {
+ indeterminate: PropTypes.bool,
+ checked: PropTypes.bool,
+ disabled: PropTypes.bool,
+ onChange: PropTypes.func
+}
+Checkbox.defaultProps = {
+ indeterminate: false,
+ checked: false,
+ disabled: false
+}
+
+export default Checkbox
diff --git a/ui/src/components/Checkbox/Checkbox.scss b/ui/src/components/Checkbox/Checkbox.scss
new file mode 100644
index 000000000..7882706ee
--- /dev/null
+++ b/ui/src/components/Checkbox/Checkbox.scss
@@ -0,0 +1,57 @@
+@import 'style/colors';
+@import "../styles/inputs";
+
+$size: 14px;
+$default-border: solid 1px $active-border-color;
+
+@mixin box() {
+ width: $size;
+ height: $size;
+ border-radius: 2px;
+ flex-shrink: 0;
+ flex-grow: 0;
+}
+
+.checkbox {
+ @include box();
+ position: relative;
+ cursor: pointer;
+ font-size: 12px;
+
+ input[type='checkbox'] {
+ cursor: inherit;
+ opacity: 0;
+ width: $size;
+ height: $size;
+ margin: 0;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+
+ &.disabled {
+ cursor: default;
+ background-color: $default-border-color;
+ }
+}
+
+.checkbox--checked {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: $active-border-color;
+ color: $white;
+ &.disabled {
+ color: $checkbox-disabled-color;
+ }
+}
+
+.checkbox--unchecked {
+ border: $default-border;
+ background-color: $white;
+
+ &.disabled {
+ border: solid 1px $checkbox-disabled-border;
+ }
+}
+
diff --git a/ui/src/components/Checkbox/style/_colors.scss b/ui/src/components/Checkbox/style/_colors.scss
new file mode 100644
index 000000000..ab96d68d7
--- /dev/null
+++ b/ui/src/components/Checkbox/style/_colors.scss
@@ -0,0 +1,4 @@
+@import "../../styles/colors";
+
+$checkbox-disabled-color: #c2c2c2;
+$checkbox-disabled-border: #dbdbdb;
diff --git a/ui/src/components/ReactTable/ReactTable.js b/ui/src/components/ReactTable/ReactTable.js
index 06de3aab6..ce219ccbf 100644
--- a/ui/src/components/ReactTable/ReactTable.js
+++ b/ui/src/components/ReactTable/ReactTable.js
@@ -211,7 +211,7 @@ ReactTableComponent.defaultProps = {
resizable: true,
tableRowId: 'id',
rowHeight: null,
- cellPadding: '12px',
+ cellPadding: '8px',
colors: {
background: {
default: '#fff',
diff --git a/ui/src/components/ReactTable/style/table-style.scss b/ui/src/components/ReactTable/style/table-style.scss
index b877d71e3..0cd86b266 100644
--- a/ui/src/components/ReactTable/style/table-style.scss
+++ b/ui/src/components/ReactTable/style/table-style.scss
@@ -54,4 +54,4 @@ $left-spacing: 0.7rem;
@include header-border;
}
-.tbody { overflow: visible!important;}
\ No newline at end of file
+.tbody { overflow: visible!important;}
diff --git a/ui/src/features/components/Box/style.scss b/ui/src/features/components/Box/style.scss
index 7bf77afb0..9689caea4 100644
--- a/ui/src/features/components/Box/style.scss
+++ b/ui/src/features/components/Box/style.scss
@@ -6,6 +6,8 @@
background-color: #fafbfc;
border: 1px solid #e9e9e9;
margin-left: 6px;
+ margin-top: 1px;
+ margin-bottom: 1px;
}
.box-title {
@@ -32,4 +34,8 @@
font-size: 21px;
font-weight: 300;
color: #778195;
-}
\ No newline at end of file
+ align-items: center;
+ display: flex;
+ justify-content: center;
+
+}
diff --git a/ui/src/features/components/Report/Charts.js b/ui/src/features/components/Report/Charts.js
new file mode 100644
index 000000000..9f7ad3b10
--- /dev/null
+++ b/ui/src/features/components/Report/Charts.js
@@ -0,0 +1,169 @@
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Legend,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis
+} from "recharts";
+import React from "react";
+import _ from "lodash";
+import Checkbox from "../../../components/Checkbox/Checkbox";
+
+const COLORS = [{stroke: "#8884d8", fill: "#8884d8"},
+ {stroke: "#82ca9d", fill: "#82ca9d"},
+ {stroke: "#ffc658", fill: "#ffc658"},
+ {stroke: "#0935FC", fill: "#0935FC"},
+ {stroke: "#395B56", fill: "#395B56"},
+ {stroke: "#617A70", fill: "#617A70"},
+ {stroke: "#CCC39F", fill: "#CCC39F"},
+ {stroke: "#FFFAD1", fill: "#FFFAD1"},
+];
+const COLOR_FAMILY = {
+ p95: [{stroke: "#BBDEF0", fill: "#BBDEF0"}, {stroke: "#00A6A6", fill: "#00A6A6"}, {
+ stroke: "#EFCA08",
+ fill: "#EFCA08"
+ }, {stroke: "#F49F0A", fill: "#F49F0A"}, {stroke: "#F08700", fill: "#F08700"}],
+ p99: [{stroke: "#134611", fill: "#134611"}, {stroke: "#3E8914", fill: "#3E8914"}, {
+ stroke: "#3DA35D",
+ fill: "#3DA35D"
+ }, {stroke: "#96E072", fill: "#96E072"}, {stroke: "#ACFC4B", fill: "#ACFC4B"}],
+ median: [{stroke: "#353531", fill: "#353531"}, {stroke: "#EC4E20", fill: "#EC4E20"}, {
+ stroke: "#FF9505",
+ fill: "#FF9505"
+ }, {stroke: "#016FB9", fill: "#016FB9"}, {stroke: "#000000", fill: "#000000"}]
+};
+const getColor = (key, index) => {
+ const prefix = key.substring(0, 1);
+ if (!(prefix.charCodeAt(0) >= 'A'.charCodeAt(0) && key.charAt(1) === '_')) {
+ // its not with A_ prefix
+ return COLORS[index % COLORS.length];
+ }
+
+ const name = key.substring(2);
+ const family = COLOR_FAMILY[name] || COLORS;
+ const loc = prefix.charCodeAt(0) - 'A'.charCodeAt(0);
+ if (family) {
+ return family[loc % family.length];
+ }
+ return COLORS[loc % COLORS.length];
+};
+
+const filterKeysFromArrayOfObject = (data, graphType, filteredKeys) => {
+ const keysToFilter = Object.keys(_.pickBy(filteredKeys, (value) => value));
+ const filteredData = data.reduce((acc, cur) => {
+ acc.push(_.omitBy(cur, (value, key) => {
+ return keysToFilter.includes(`${graphType}${key}`)
+ }));
+ return acc;
+ }, []);
+
+ return filteredData;
+};
+
+export const BarChartPredator = ({data = [], keys=[], graphType, onSelectedGraphPropertyFilter, filteredKeys}) => {
+ const filteredData = filterKeysFromArrayOfObject(data, graphType, filteredKeys);
+
+ return (
+
+
+
+
+
+
+
+ )
+};
+
+export const LineChartPredator = ({data = [], keys = [], labelY, graphType, onSelectedGraphPropertyFilter, filteredKeys}) => {
+ const filteredData = filterKeysFromArrayOfObject(data, graphType, filteredKeys);
+
+ return (
+
+
+
+
+ Math.round(dataMax * 1.1)]}/>
+
+
+ )
+}
+
+
+const renderLegend = (props) => {
+ const {payload, onSelectedGraphPropertyFilter, graphType, filteredKeys} = props;
+ if (payload.length === 1) {
+ return null;
+ }
+ return (
+
+ {
+ payload.map((entry, index) => (
+
+ {
+ onSelectedGraphPropertyFilter(graphType, entry.value, value)
+ }}
+ />
+ {entry.value}
+
+ ))
+ }
+
+ );
+}
diff --git a/ui/src/features/components/Report/compareReports.js b/ui/src/features/components/Report/compareReports.js
new file mode 100644
index 000000000..5218b055b
--- /dev/null
+++ b/ui/src/features/components/Report/compareReports.js
@@ -0,0 +1,301 @@
+import React from 'react';
+
+import Modal from '../Modal';
+import {prettySeconds} from '../../utils';
+import PieChart from '../PieChart'
+import _ from 'lodash';
+import {LineChartPredator, BarChartPredator} from './Charts'
+
+import * as Actions from "../../redux/actions/reportsActions";
+import * as selectors from "../../redux/selectors/reportsSelector";
+import {connect} from "react-redux";
+import Snackbar from "material-ui/Snackbar";
+import Checkbox from "../../../components/Checkbox/Checkbox";
+import Button from "../../../components/Button";
+
+
+class CompareReports extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ reportsList: [],
+ mergedReports: this.mergeGraphs([]),
+ filteredKeys: {}
+ };
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (prevProps.aggregateReports !== this.props.aggregateReports) {
+ const reportsList = this.props.aggregateReports.map((report) => ({
+ name: report.alias,
+ startTime: report.startTime,
+ testName: report.testName,
+ duration: report.duration,
+ show: true
+ }));
+
+ const keysToDefaultFilter = reportsList.flatMap((reportInfo) => [`${reportInfo.name}_p95`, `${reportInfo.name}_p99`]);
+ this.onSelectedGraphPropertyFilter('latency', keysToDefaultFilter, false);
+ this.setState({reportsList});
+ this.setMergedReports(reportsList)
+ }
+
+ }
+
+ setMergedReports = (reportsList) => {
+ const reportsNames = reportsList.filter(cur => cur.show).map(cur => cur.name);
+ const {aggregateReports} = this.props;
+ const filteredData = aggregateReports.filter((report) => reportsNames.includes(report.alias));
+ const mergedReports = this.mergeGraphs(filteredData);
+ this.setState({mergedReports});
+ };
+
+
+ createBenchmark = () => {
+ const {aggregateReport, report} = this.props;
+ this.props.createBenchmark(report.test_id, aggregateReport.benchMark);
+ };
+ onSelectedReport = (value, index) => {
+ const {reportsList} = this.state;
+ reportsList[index].show = value;
+ this.setState({reportsList: [...reportsList]});
+ this.setMergedReports(reportsList);
+ };
+ onSelectedGraphPropertyFilter = (graphType, keys, value) => {
+ const {filteredKeys} = this.state;
+ let newFilteredKeys = {...filteredKeys};
+ if (_.isArray(keys)) {
+ newFilteredKeys = keys.reduce((acc, cur) => {
+ acc[`${graphType}${cur}`] = !value;
+ return acc;
+ }, filteredKeys)
+ } else {
+ newFilteredKeys[`${graphType}${keys}`] = !value;
+ }
+ this.setState({filteredKeys: {...newFilteredKeys}});
+ };
+
+ render() {
+ const {reportsList, mergedReports, filteredKeys} = this.state;
+ const {onClose} = this.props;
+ return (
+
+
+
Compare reports
+
+
+
Overall Latency
+
+ Status Codes
+
+ RPS
+
+ Status Codes And Errors Distribution
+
+
+
+
+
+
+
+ {
+ this.props.createBenchmarkSuccess(false);
+ }}
+ />
+
+ );
+ }
+
+
+ loadData = () => {
+ const {getAggregateReports, selectedReports} = this.props;
+ const selectedReportsAsList = Object.entries(selectedReports)
+ .flatMap(selectedReport => {
+ const testId = selectedReport[0];
+ const selectedList = Object.entries(selectedReport[1])
+ .filter((isSelected) => isSelected[1])
+ .map((pairs) => pairs[0]);
+ return selectedList.map((reportId) => {
+ return {
+ testId,
+ reportId
+ }
+ })
+ });
+ getAggregateReports(selectedReportsAsList);
+ };
+
+ mergeGraphs = (data) => {
+ const initial = {
+ latencyGraph: [],
+ latencyGraphKeys: [],
+ errorsCodeGraph: [],
+ errorsCodeGraphKeys: [],
+ rps: [],
+ rpsKeys: [],
+ errorsBar: [],
+ errorsBarKeys: [],
+ scenarios: []
+ };
+ if (data.length === 0) {
+ return initial;
+ }
+
+ //merged sorted data by time
+ const result = data.reduce((acc, cur) => {
+ acc.latencyGraph = mergeSortedArraysByStartTime(acc.latencyGraph, cur.latencyGraph);
+ acc.latencyGraphKeys = acc.latencyGraphKeys.concat(cur.latencyGraphKeys);
+
+ acc.errorsCodeGraph = mergeSortedArraysByStartTime(acc.errorsCodeGraph, cur.errorsCodeGraph);
+ acc.errorsCodeGraphKeys = acc.errorsCodeGraphKeys.concat(cur.errorsCodeGraphKeys);
+
+ acc.rps = mergeSortedArraysByStartTime(acc.rps, cur.rps);
+ acc.rpsKeys = acc.rpsKeys.concat(cur.rpsKeys);
+
+ acc.errorsBar = acc.errorsBar.concat(cur.errorsBar);
+ acc.errorsBarKeys = acc.errorsBarKeys.concat(cur.errorsBarKeys);
+
+ acc.scenarios = acc.scenarios.concat(cur.scenarios);
+ return acc;
+ }, initial);
+
+ return result;
+ }
+
+
+ componentDidMount() {
+ this.loadData();
+ }
+
+ componentWillUnmount() {
+ this.props.getAggregateReportSuccess([])
+ }
+};
+
+
+function mapStateToProps(state) {
+ return {
+ aggregateReports: selectors.getAggregateReportsForCompare(state),
+ createBenchmarkSucceed: selectors.createBenchmarkSuccess(state),
+ }
+}
+
+const mapDispatchToProps = {
+ getAggregateReports: Actions.getAggregateReports,
+ createBenchmark: Actions.createBenchmark,
+ createBenchmarkSuccess: Actions.createBenchmarkSuccess,
+ getAggregateReportSuccess: Actions.getAggregateReportSuccess,
+};
+
+const Block = ({header, dataList, style = {}}) => {
+ const headerStyle = {color: '#577DFE', fontWeight: '500'};
+
+ return (
+
+
{header}
+ {dataList.map((element, index) => (
+
{element}
))}
+
+ )
+
+};
+
+const ReportsList = ({list = [], onChange}) => {
+ const headerStyle = {marginRight: '10px'};
+ const data = list.reduce((acc, cur, index) => {
+ acc.symbols.push(cur.name);
+ acc.testNames.push(cur.testName);
+ acc.durations.push(prettySeconds(cur.duration));
+ acc.startTimes.push(cur.startTime);
+ acc.checkboxes.push( onChange(value, index)}
+ />);
+ return acc;
+ }, {
+ symbols: [],
+ testNames: [],
+ durations: [],
+ startTimes: [],
+ checkboxes: [],
+ });
+
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(CompareReports);
+
+
+function mergeSortedArraysByStartTime(arr1, arr2) {
+ const arr3 = [];
+
+ let i = 0, j = 0;
+ while (i < arr1.length && j < arr2.length) {
+
+ if (arr1[i].timeMills < arr2[j].timeMills) {
+ arr3.push(arr1[i]);
+ i++;
+ } else if (arr1[i].timeMills === arr2[j].timeMills) {
+ const newData = {...arr1[i], ...arr2[j]};
+ arr3.push(newData);
+ i++;
+ j++;
+ } else {
+ arr3.push(arr2[j]);
+ j++;
+ }
+
+ }
+ while (i < arr1.length) {
+ arr3.push(arr1[i]);
+ i++;
+ }
+ while (j < arr2.length) {
+ arr3.push(arr2[j]);
+ j++;
+ }
+ return arr3;
+}
+
diff --git a/ui/src/features/components/Report/index.js b/ui/src/features/components/Report/index.js
index 4dc21d953..87e58fe7e 100644
--- a/ui/src/features/components/Report/index.js
+++ b/ui/src/features/components/Report/index.js
@@ -3,14 +3,6 @@ import React from 'react';
import Modal from '../Modal';
import {prettySeconds} from '../../utils';
import PieChart from '../PieChart'
-import {
- AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
- LineChart,
- Legend,
- BarChart, Bar,
- Line
-} from 'recharts';
-
import * as Actions from "../../redux/actions/reportsActions";
import * as selectors from "../../redux/selectors/reportsSelector";
import {connect} from "react-redux";
@@ -18,6 +10,8 @@ import Box from '../Box';
import dateFormat from 'dateformat';
import Button from '../../../components/Button';
import Snackbar from "material-ui/Snackbar";
+import {BarChartPredator, LineChartPredator} from "./Charts";
+import _ from "lodash";
const REFRESH_DATA_INTERVAL = 30000;
const COLORS = [{stroke: "#8884d8", fill: "#8884d8"},
@@ -30,97 +24,32 @@ class Report extends React.Component {
constructor(props) {
super(props);
this.state = {
- disabledCreateBenchmark: false
+ disabledCreateBenchmark: false,
+ filteredKeys: {}
}
}
- generateAreaChart = (data, keys, labelY) => {
- return (
-
-
-
-
- Math.round(dataMax * 1.1)]}/>
-
-
- {
- keys.map((key, index) => {
- const color = COLORS[index % COLORS.length];
- return ()
- })
- }
-
-
- )
- }
- lineChart = (data, keys, labelY) => {
- return (
-
-
-
-
- Math.round(dataMax * 1.1)]}/>
-
-
- {
- keys.map((key, index) => {
- const color = COLORS[index % COLORS.length];
- return ()
- })
- }
-
-
- )
- }
- barChart = (data, keys) => {
- return (
-
-
-
-
-
-
- {
- keys.map((key, index) => {
- const color = COLORS[index % COLORS.length];
- return ()
- })
- }
-
-
- )
- };
createBenchmark = () => {
const {aggregateReport, report} = this.props;
this.props.createBenchmark(report.test_id, aggregateReport.benchMark);
this.setState({disabledCreateBenchmark: true})
};
-
+ onSelectedGraphPropertyFilter = (graphType, keys, value) => {
+ const {filteredKeys} = this.state;
+ let newFilteredKeys = {...filteredKeys};
+ if (_.isArray(keys)) {
+ newFilteredKeys = keys.reduce((acc, cur) => {
+ acc[`${graphType}${cur}`] = !value;
+ return acc;
+ }, filteredKeys)
+ } else {
+ newFilteredKeys[`${graphType}${keys}`] = !value;
+ }
+ this.setState({filteredKeys: {...newFilteredKeys}});
+ };
render() {
const {report, onClose, aggregateReport} = this.props;
- const {disabledCreateBenchmark} = this.state;
+ const {disabledCreateBenchmark,filteredKeys} = this.state;
return (
-
{report.test_name}
+ {report.test_name}
Started at {dateFormat(new Date(report.start_time), "dddd, mmmm dS, yyyy, h:MM:ss TT")}
@@ -144,15 +73,28 @@ class Report extends React.Component {
- {this.lineChart(aggregateReport.latencyGraph, ['median', 'p95', 'p99'], 'ms')}
+
+
Status Codes
- {this.lineChart(aggregateReport.errorsCodeGraph, Object.keys(aggregateReport.errorCodes))}
+
RPS
- {this.generateAreaChart(aggregateReport.rps, ['mean'])}
+
Status Codes And Errors Distribution
- {this.barChart(aggregateReport.errorsBar, ['count'])}
+
Scenarios
@@ -177,8 +119,8 @@ class Report extends React.Component {
}
loadData = () => {
- const {getAggregateReport, report} = this.props;
- getAggregateReport(report.test_id, report.report_id);
+ const {getAggregateReports, report} = this.props;
+ getAggregateReports([{testId:report.test_id, reportId:report.report_id}]);
}
@@ -203,7 +145,7 @@ function mapStateToProps(state) {
}
const mapDispatchToProps = {
- getAggregateReport: Actions.getAggregateReport,
+ getAggregateReports: Actions.getAggregateReports,
createBenchmark: Actions.createBenchmark,
createBenchmarkSuccess: Actions.createBenchmarkSuccess,
};
@@ -211,10 +153,11 @@ const mapDispatchToProps = {
const SummeryTable = ({report = {}}) => {
return (
-
+
+ {report.score && }
);
}
diff --git a/ui/src/features/components/TestForm/collapsibleScenarioConfig.js b/ui/src/features/components/TestForm/collapsibleScenarioConfig.js
index 0669ca53d..93cb70ef1 100644
--- a/ui/src/features/components/TestForm/collapsibleScenarioConfig.js
+++ b/ui/src/features/components/TestForm/collapsibleScenarioConfig.js
@@ -37,12 +37,16 @@ export default class CollapsibleScenarioConfig extends React.Component {
{
evt.stopPropagation();
onDuplicateScenario()
- }} icon='fa-copy' tooltip='Duplicate scenario'/>,
- {
+ }} icon='fa-copy' tooltip='Duplicate scenario'/>
+ ]
+
+ if(onDeleteScenario){
+ sections.push( {
evt.stopPropagation();
onDeleteScenario()
- }} icon='fa-trash' tooltip='Delete scenario' borderLeft/>,
- ]
+ }} icon='fa-trash' tooltip='Delete scenario' borderLeft/>)
+ }
+
const {expanded} = this.state;
return ( {
diff --git a/ui/src/features/components/TestForm/dragableWrapper.js b/ui/src/features/components/TestForm/dragableWrapper.js
new file mode 100644
index 000000000..c6f7d27a1
--- /dev/null
+++ b/ui/src/features/components/TestForm/dragableWrapper.js
@@ -0,0 +1,112 @@
+import * as React from 'react'
+import {findDOMNode} from 'react-dom'
+import {
+ DragSource,
+ DropTarget,
+ ConnectDropTarget,
+ ConnectDragSource,
+ DropTargetMonitor,
+ DropTargetConnector,
+ DragSourceConnector,
+ DragSourceMonitor
+} from 'react-dnd'
+
+const style = {
+ border: '1px dashed gray',
+ padding: '0.5rem 1rem',
+ marginBottom: '.5rem',
+ backgroundColor: 'white',
+ cursor: 'move'
+}
+
+const cardSource = {
+ beginDrag(props) {
+ return {
+ id: props.id,
+ index: props.index
+ }
+ }
+};
+
+const cardTarget = {
+ hover(props, monitor, component) {
+ if (!component) {
+ return null
+ }
+ const dragIndex = monitor.getItem().index;
+ const hoverIndex = props.index;
+
+ // Don't replace items with themselves
+ if (dragIndex === hoverIndex) {
+ return
+ }
+
+ // Determine rectangle on screen
+ const hoverBoundingRect = (findDOMNode(component)).getBoundingClientRect();
+
+ // Get vertical middle
+ const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
+
+ // Determine mouse position
+ const clientOffset = monitor.getClientOffset();
+
+ // Get pixels to the top
+ const hoverClientY = clientOffset.y - hoverBoundingRect.top;
+
+ // Only perform the move when the mouse has crossed half of the items height
+ // When dragging downwards, only move when the cursor is below 50%
+ // When dragging upwards, only move when the cursor is above 50%
+
+ // Dragging downwards
+ if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
+ return
+ }
+
+ // Dragging upwards
+ if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
+ return
+ }
+
+ // Time to actually perform the action
+ props.move(dragIndex, hoverIndex)
+
+ // Note: we're mutating the monitor item here!
+ // Generally it's better to avoid mutations,
+ // but it's good here for the sake of performance
+ // to avoid expensive index searches.
+ monitor.getItem().index = hoverIndex
+ }
+};
+
+class Card extends React.Component {
+ render() {
+ const {
+ isDragging,
+ connectDragSource,
+ connectDropTarget,
+ children
+ } = this.props;
+ const opacity = isDragging ? 0 : 1;
+ return connectDragSource(
+ connectDropTarget({children}
)
+ )
+ }
+}
+
+export default DropTarget(
+ 'card',
+ cardTarget,
+ (connect) => ({
+ connectDropTarget: connect.dropTarget()
+ })
+)(
+ DragSource(
+ 'card',
+ cardSource,
+ (connect, monitor) => ({
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging()
+ })
+ )(Card))
+
+
diff --git a/ui/src/features/components/TestForm/index.js b/ui/src/features/components/TestForm/index.js
index 2aab52e98..b79c6b3a9 100644
--- a/ui/src/features/components/TestForm/index.js
+++ b/ui/src/features/components/TestForm/index.js
@@ -53,7 +53,7 @@ export class TestForm extends React.Component {
const {maxSupportedScenariosUi} = this.state;
const {cleanAllErrors} = this.props;
cleanAllErrors();
- if(maxSupportedScenariosUi){
+ if (maxSupportedScenariosUi) {
this.setState({maxSupportedScenariosUi: null})
}
@@ -92,6 +92,7 @@ export class TestForm extends React.Component {
const {createTestError, processorsError, closeDialog, processorsLoading, processorsList} = this.props;
const {name, description, baseUrl, processorId, editMode, maxSupportedScenariosUi} = this.state;
const error = createTestError || processorsError || maxSupportedScenariosUi;
+
return (
@@ -256,7 +257,13 @@ export class TestForm extends React.Component {
onDeleteScenario = () => {
const {scenarios, currentScenarioIndex} = this.state;
scenarios.splice(currentScenarioIndex, 1);
- this.setState({scenarios, currentScenarioIndex: currentScenarioIndex - 1});
+ let newCurrentScenarioIndex;
+ if(currentScenarioIndex===0){
+ newCurrentScenarioIndex=0;
+ }else{
+ newCurrentScenarioIndex =currentScenarioIndex-1;
+ }
+ this.setState({scenarios, currentScenarioIndex: newCurrentScenarioIndex});
};
onDuplicateScenario = () => {
@@ -278,7 +285,19 @@ export class TestForm extends React.Component {
}
return steps;
};
-
+ updateStepOrder = (dragIndex, hoverIndex) => {
+ const {scenarios, currentScenarioIndex, before} = this.state;
+ let steps;
+ if (currentScenarioIndex === null) {
+ steps = before.steps;
+ } else {
+ steps = scenarios[currentScenarioIndex].steps;
+ }
+ const step = steps[dragIndex];
+ steps.splice(dragIndex, 1);
+ steps.splice(hoverIndex, 0, step);
+ this.setState({scenarios, before});
+ };
calcMaxAllowedWeight = (index) => {
const {scenarios, currentScenarioIndex} = this.state;
const exceptIndex = index || currentScenarioIndex;
@@ -318,7 +337,8 @@ export class TestForm extends React.Component {
+Add Step
+Add Before
-
this.onChooseScenario(key)} activeTabKey={activeTabKey} className={style.tabs}>
+ this.onChooseScenario(key)} activeTabKey={activeTabKey}
+ className={style.tabs}>
{
tabsData.map((tabData, index) => {
return (
@@ -339,7 +359,7 @@ export class TestForm extends React.Component {
scenario={tabData}
onChangeValueOfScenario={this.onChangeValueOfScenario}
processorsExportedFunctions={processorsExportedFunctions}
- onDeleteScenario={this.onDeleteScenario}
+ onDeleteScenario={scenarios.length === 1 ? undefined : this.onDeleteScenario}
onDuplicateScenario={this.onDuplicateScenario}
/>
@@ -351,6 +371,7 @@ export class TestForm extends React.Component {
processorsExportedFunctions={processorsExportedFunctions}
onDeleteStep={this.onDeleteStep}
onDuplicateStep={this.onDuplicateStep}
+ updateStepOrder={this.updateStepOrder}
/>
diff --git a/ui/src/features/components/TestForm/stepsList.js b/ui/src/features/components/TestForm/stepsList.js
index 45a15e13d..4c3366df5 100644
--- a/ui/src/features/components/TestForm/stepsList.js
+++ b/ui/src/features/components/TestForm/stepsList.js
@@ -1,6 +1,6 @@
import React from 'react';
import CollapsibleStep from './collapsibleStep';
-
+import DragableWrapper from './dragableWrapper'
export default class StepsList extends React.Component {
constructor(props) {
super(props);
@@ -16,6 +16,7 @@ export default class StepsList extends React.Component {
afterStepProcessorValue,
onDeleteStep,
onDuplicateStep,
+ updateStepOrder,
} = this.props;
return ( {
return (
+
+
)
})
diff --git a/ui/src/features/configurationColumn.js b/ui/src/features/configurationColumn.js
index eeb266308..25fa3f844 100644
--- a/ui/src/features/configurationColumn.js
+++ b/ui/src/features/configurationColumn.js
@@ -1,6 +1,7 @@
import {TableHeader} from "../components/ReactTable";
import React, {useEffect, useState} from "react";
import {get} from 'lodash';
+import Checkbox from '../components/Checkbox/Checkbox';
import Moment from 'moment';
import prettySeconds from 'pretty-seconds';
@@ -23,21 +24,35 @@ import TooltipWrapper from '../components/TooltipWrapper';
import {getTimeFromCronExpr} from './utils';
import UiSwitcher from '../components/UiSwitcher';
import TextArea from "../components/TextArea";
-import TitleInput from "../components/TitleInput";
import ClickOutHandler from 'react-onclickout'
-
-export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, onRawView, onStop, onDelete, onEdit, onRunTest, onEnableDisable, onEditNote}) => {
+const iconsWidth = 50;
+const mediumSize = 60;
+const semiLarge = 70;
+const largeSize = 85;
+const extraLargeSize = 100;
+const extraExLargeSize = 120;
+export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, onRawView, onStop, onDelete, onEdit, onRunTest, onEnableDisable, onEditNote, selectedReports, onReportSelected}) => {
const columns = [
{
+ id: 'compare',
+ Header: () => (
+
+ Select
+
+ ),
+ accessor: (data) =>
,
+ width: iconsWidth
+ }, {
id: 'report_id',
Header: () => (
Test Name
),
- accessor: 'report_id'
+ accessor: 'report_id',
},
{
id: 'name',
@@ -47,8 +62,6 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
),
accessor: 'name',
- headerClassName: css['header-name'],
- className: css['header-name']
}, {
id: 'processor_name',
Header: () => (
@@ -57,8 +70,6 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
),
accessor: 'name',
- headerClassName: css['header-name'],
- className: css['header-name']
},
{
id: 'description',
@@ -67,7 +78,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Description
),
- accessor: 'description'
+ accessor: 'description',
+ className: css['center-flex'],
}, {
id: 'updated_at',
Header: () => (
@@ -81,7 +93,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Modified
),
- accessor: (data) => (dateFormatter(data.updated_at))
+ accessor: (data) => (dateFormatter(data.updated_at)),
+ width: extraExLargeSize + 20,
+ className: css['center-flex'],
}, {
id: 'type',
Header: () => (
@@ -90,8 +104,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
),
accessor: 'type',
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: iconsWidth,
+ className: css['center-flex'],
}, {
id: 'edit',
Header: () => (
@@ -116,8 +130,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
N/A
,
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: iconsWidth,
+ className: css['center-flex'],
},
{
id: 'processor_edit',
@@ -130,8 +144,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
onEdit(data)
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: iconsWidth,
+ className: css['center-flex'],
},
{
id: 'test_name',
@@ -141,8 +155,7 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
),
accessor: 'test_name',
- headerClassName: css['header-name'],
- className: css['header-name']
+
},
{
@@ -176,7 +189,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
End Time
),
- accessor: data => ({dateFormatter(data.end_time)}
)
+ accessor: data => ({dateFormatter(data.end_time)}
),
+ width: extraLargeSize,
+ className: css['center-flex'],
},
{
id: 'duration',
@@ -185,7 +200,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Duration
),
- accessor: data => (prettySeconds(data.duration))
+ accessor: data => (prettySeconds(data.duration)),
+ width: largeSize,
+ className: css['center-flex'],
},
{
id: 'status',
@@ -194,7 +211,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Status
),
- accessor: data => statusFormatter(data.status)
+ accessor: data => statusFormatter(data.status),
+ width: mediumSize,
+ className: css['center-flex'],
},
{
id: 'arrival_rate',
@@ -203,7 +222,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Arrival Rate
),
- accessor: 'arrival_rate'
+ accessor: 'arrival_rate',
+ width: largeSize,
+ className: css['center-flex'],
},
{
id: 'ramp_to',
@@ -212,7 +233,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Ramp To
),
- accessor: data => (data.ramp_to || 'N/A')
+ accessor: data => (data.ramp_to || 'N/A'),
+ width: largeSize,
+ className: css['center-flex'],
},
{
id: 'max_virtual_users',
@@ -221,7 +244,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Max Virtual Users
),
- accessor: data => (data.max_virtual_users || 'N/A')
+ accessor: data => (data.max_virtual_users || 'N/A'),
+ width: extraExLargeSize,
+ className: css['center-flex'],
},
{
id: 'cron_expression',
@@ -230,7 +255,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Cron Expression
),
- accessor: data => (getTimeFromCronExpr(data.cron_expression) || 'N/A')
+ accessor: data => (getTimeFromCronExpr(data.cron_expression) || 'N/A'),
+ width: extraExLargeSize,
+ className: css['center-flex'],
},
{
id: 'last_run',
@@ -250,16 +277,20 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Success Rate
),
- accessor: data => (Math.floor(data.last_success_rate) + '%')
+ accessor: data => (Math.floor(data.last_success_rate) + '%'),
+ width: extraLargeSize,
+ className: css['center-flex'],
},
{
- id: 'last_rps',
+ id: 'avg_rps',
Header: () => (
RPS
),
- accessor: data => (Math.floor(data.last_rps))
+ accessor: data => (Math.floor(data.avg_rps === undefined ? data.last_rps : data.avg_rps)),
+ width: iconsWidth,
+ className: css['center-flex'],
},
{
@@ -269,7 +300,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Parallelism
),
- accessor: 'parallelism'
+ accessor: 'parallelism',
+ width: largeSize,
+ className: css['center-flex'],
},
{
id: 'notes',
@@ -278,7 +311,7 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
Notes
),
- accessor: data =>
+ accessor: data => ,
},
{
id: 'score',
@@ -291,12 +324,11 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
if (data.score) {
const color = get(data, 'benchmark_weights_data.benchmark_threshold', 0) <= data.score ? 'green' : 'red';
return (
- {Math.floor(data.score)}
+ {Math.floor(data.score)}
)
}
},
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: iconsWidth
}, {
id: 'report',
Header: () => (
@@ -308,8 +340,7 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
onReportView(data)
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: mediumSize
},
{
id: 'grafana_report',
@@ -322,8 +353,7 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
window.open(data.grafana_report, '_blank')
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: mediumSize
},
{
id: 'raw',
@@ -336,9 +366,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
onRawView(data)
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header'],
-
+ width: iconsWidth,
+ className: css['center-flex'],
},
{
id: 'rerun',
@@ -351,9 +380,7 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
onRunTest(data)
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header'],
-
+ width: iconsWidth
},
{
id: 'run_now',
@@ -366,9 +393,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
onRunTest(data)
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header'],
-
+ width: largeSize,
+ className: css['center-flex'],
},
{
id: 'delete',
@@ -381,8 +407,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
onDelete(data)
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: mediumSize,
+ className: css['center-flex'],
},
{
id: 'run_test',
@@ -395,8 +421,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
onRunTest(data)
}}/>,
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: semiLarge,
+ className: css['center-flex'],
}, {
id: 'logs',
Header: () => (
@@ -409,8 +435,8 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
e.stopPropagation();
window.open(`${env.PREDATOR_URL}/jobs/${data.job_id}/runs/${data.report_id}/logs`, '_blank')
}}/>),
- className: css['small-header'],
- headerClassName: css['small-header']
+ width: iconsWidth
+
}, {
id: 'stop',
Header: () => (
@@ -425,9 +451,7 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
onStop(data)
}}/>)
},
- className: css['small-header'],
- headerClassName: css['small-header']
-
+ width: iconsWidth
},
{
id: 'enabled_disabled',
@@ -439,24 +463,24 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView,
accessor: (data) => {
const activated = (typeof data.enabled === 'undefined' ? true : data.enabled);
return (
- {
- onEnableDisable(data, value)
- }}
- disabledInp={false}
- activeState={activated}
- height={12}
- width={22}
- />)
+
+ {
+ onEnableDisable(data, value)
+ }}
+ disabledInp={false}
+ activeState={activated}
+ height={12}
+ width={22}
+ />
+
)
},
- className: css['small-header'],
- headerClassName: css['small-header']
-
+ width: semiLarge,
+ className: css['center-flex'],
}
];
- // return filter(columns, (column) => columnsNames.includes(column.id))
return columnsNames.map((name) => {
const column = columns.find((c) => c.id === name);
if (!column) {
@@ -489,6 +513,19 @@ const ViewButton = ({onClick, icon, disabled, text}) => {
return ({element}
)
};
+const CompareCheckbox = ({data, onReportSelected, selectedReports}) => {
+
+ return (
+
+ onReportSelected(data.test_id, data.report_id, value)}
+ />
+
+ )
+}
const Notes = ({data, onEditNote}) => {
const {notes = '', report_id, test_id} = data;
diff --git a/ui/src/features/configurationColumn.scss b/ui/src/features/configurationColumn.scss
index 511c9cfd6..04c1800e3 100644
--- a/ui/src/features/configurationColumn.scss
+++ b/ui/src/features/configurationColumn.scss
@@ -12,6 +12,13 @@
height:46px;
display:flex;
align-items: center;
+ justify-content: center;
+}
+
+.center-flex {
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.disabled-button {
@@ -30,14 +37,3 @@
text-overflow: ellipsis;
max-width: 164px;
}
-
-.header-name {
- width:15% !important;
-}
-
-.small-header {
- margin:auto;
- width:50px !important;
-}
-
-
diff --git a/ui/src/features/get-last-reports.js b/ui/src/features/get-last-reports.js
index b6fd4de51..24ccb7c9b 100644
--- a/ui/src/features/get-last-reports.js
+++ b/ui/src/features/get-last-reports.js
@@ -10,17 +10,20 @@ import * as Actions from './redux/action';
import Page from '../components/Page';
import _ from 'lodash';
import Report from './components/Report';
+import CompareReports from "./components/Report/compareReports";
import {createJobRequest} from './requestBuilder';
import {ReactTableComponent} from './../components/ReactTable';
import {getColumns} from './configurationColumn'
import ErrorDialog from "./components/ErrorDialog";
import FormWrapper from "../components/FormWrapper";
+import Button from "../components/Button";
+import Loader from "./components/Loader";
const REFRESH_DATA_INTERVAL = 30000;
-const columnsNames = ['test_name', 'start_time', 'end_time', 'duration', 'status', 'arrival_rate',
- 'ramp_to', 'last_success_rate', 'last_rps', 'parallelism', 'notes', 'score', 'report', 'grafana_report', 'rerun', 'raw', 'logs', 'stop'];
+const columnsNames = ['compare', 'test_name', 'start_time', 'end_time', 'duration', 'status', 'arrival_rate',
+ 'ramp_to', 'last_success_rate', 'avg_rps', 'parallelism', 'notes', 'score', 'report', 'grafana_report', 'rerun', 'raw', 'logs', 'stop'];
const DESCRIPTION = 'Reports give you insight into the performance of your API. Predator generates a report for each test that is executed.';
class getReports extends React.Component {
@@ -31,7 +34,8 @@ class getReports extends React.Component {
showReport: false,
sortedReports: [],
sortHeader: '',
- rerunJob: null
+ rerunJob: null,
+ showCompareReports: false
};
}
@@ -55,6 +59,7 @@ class getReports extends React.Component {
this.setState({rerunJob: job});
};
+
onEditNote = (testId, reportId, notes) => {
const {editReport} = this.props;
editReport(testId, reportId, {notes});
@@ -91,6 +96,7 @@ class getReports extends React.Component {
componentWillUnmount() {
this.props.clearSelectedReport();
clearInterval(this.refreshDataInterval);
+ this.props.clearReportForCompare();
}
onSort = (field) => {
@@ -123,15 +129,25 @@ class getReports extends React.Component {
this.props.clearErrorOnStopJob();
};
+ closeCompareReports = () => {
+ this.setState({showCompareReports: false})
+ };
+ onReportSelected = (testId, reportId, value) => {
+ this.props.addReportForCompare(testId, reportId, value);
+ };
+ loader() {
+ return this.props.processingGetReports ? : 'There is no data'
+ }
render() {
- const {showReport, sortHeader, sortedReports} = this.state;
+ const {showReport, sortHeader, sortedReports, showCompareReports} = this.state;
const {
errorOnGetReports,
errorOnGetReport,
errorOnStopRunningJob,
errorCreateBenchmark,
- errorEditReport
+ errorEditReport,
+ selectedReports
} = this.props;
const columns = getColumns({
columnsNames,
@@ -141,7 +157,9 @@ class getReports extends React.Component {
onRawView: this.onRawView,
onStop: this.onStop,
onRunTest: this.onRunTest,
- onEditNote: this.onEditNote
+ onEditNote: this.onEditNote,
+ onReportSelected: this.onReportSelected,
+ selectedReports: this.props.selectedReports
});
const feedbackMessage = this.generateFeedbackMessage();
const error = errorOnGetReports || errorOnGetReport || errorOnStopRunningJob || errorCreateBenchmark || errorEditReport;
@@ -151,6 +169,15 @@ class getReports extends React.Component {
{showReport &&
}
+
: null}
-
+ {
+ showCompareReports &&
+
+ }
{feedbackMessage && {
this.setState({showReport: null})
};
+ closeCompareReports = () => {
+ this.setState({showCompareReports: false})
+ };
+ onReportSelected = (testId, reportId, value) => {
+ this.props.addReportForCompare(testId, reportId, value);
+ };
render() {
const noDataText = this.props.errorOnGetReports ? errorMsgGetReports : this.loader();
@@ -132,12 +141,15 @@ class getTests extends React.Component {
onRawView: this.onRawView,
onStop: this.onStop,
onRunTest: this.onRunTest,
- onEditNote: this.onEditNote
+ onEditNote: this.onEditNote,
+ onReportSelected: this.onReportSelected,
+ selectedReports: this.props.selectedReports
});
- const {showReport} = this.state;
+ const {showReport, showCompareReports} = this.state;
const {
errorCreateBenchmark,
errorEditReport,
+ selectedReports,
} = this.props;
const feedbackMessage = this.generateFeedbackMessage();
const error = errorCreateBenchmark || errorEditReport;
@@ -145,6 +157,15 @@ class getTests extends React.Component {
0 && `${this.props.reports[0].test_name} Reports`}
description={DESCRIPTION}>
+
{showReport &&
}
+ {this.state.openViewReport ?