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 ( + + + + + + renderLegend({ + ...props, + graphType, + onSelectedGraphPropertyFilter, + filteredKeys + })}/> + + { + keys.map((key, index) => { + const color = getColor(key, index); + return () + }) + } + + + ) +}; + +export const LineChartPredator = ({data = [], keys = [], labelY, graphType, onSelectedGraphPropertyFilter, filteredKeys}) => { + const filteredData = filterKeysFromArrayOfObject(data, graphType, filteredKeys); + + return ( + + + + + Math.round(dataMax * 1.1)]}/> + renderLegend({ + ...props, + graphType, + onSelectedGraphPropertyFilter, + filteredKeys + })}/> + + { + keys.map((key, index) => { + const color = getColor(key, index); + return () + }) + } + + + ) +} + + +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

+ +
+

Scenarios

+ +
+
+
+
+ +
+ { + 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 ? : null} + { + showCompareReports && + + } {this.state.openViewReport ? : null} {feedbackMessage && ( - { type: Types.GET_REPORTS, testId } + {type: Types.GET_REPORTS, testId} ); export const getLastReports = () => ( - { type: Types.GET_LAST_REPORTS } + {type: Types.GET_LAST_REPORTS} ); export const getReportsSuccess = (reports) => ( - { type: Types.GET_REPORTS_SUCCESS, reports } + {type: Types.GET_REPORTS_SUCCESS, reports} ); export const getReportsFaliure = (error) => ( - { type: Types.GET_REPORTS_FAILURE, error } + {type: Types.GET_REPORTS_FAILURE, error} ); export const clearErrorOnGetReports = () => ( - { type: Types.CLEAR_ERROR_ON_GET_REPORTS } + {type: Types.CLEAR_ERROR_ON_GET_REPORTS} ); export const clearReports = () => ( - { type: Types.CLEAR_REPORTS } + {type: Types.CLEAR_REPORTS} ); export const getReport = (testId, runId) => ( - { type: Types.GET_REPORT, testId, runId } + {type: Types.GET_REPORT, testId, runId} ); export const getReportSuccess = (report) => ( - { type: Types.GET_REPORT_SUCCESS, report } + {type: Types.GET_REPORT_SUCCESS, report} ); export const getReportFaliure = (error) => ( - { type: Types.GET_REPORT_FAILURE, error } + {type: Types.GET_REPORT_FAILURE, error} ); export const clearSelectedReport = () => ( - { type: Types.CLEAR_SELECTED_REPORT } + {type: Types.CLEAR_SELECTED_REPORT} ); export const processingGetReports = (state) => ( - { type: Types.PROCESSING_GET_REPORTS, state } + {type: Types.PROCESSING_GET_REPORTS, state} ); -export const getAggregateReport = (testId,reportId) => ( - { type: Types.GET_AGGREGATE_REPORT, testId,reportId } +export const getAggregateReports = (reportsData) => ( + {type: Types.GET_AGGREGATE_REPORTS, reportsData} ); -export const createBenchmark = (testId,body) => ( - { type: Types.CREATE_BENCHMARK, testId,body } +export const createBenchmark = (testId, body) => ( + {type: Types.CREATE_BENCHMARK, testId, body} ); export const createBenchmarkSuccess = (value) => ( - { type: Types.CREATE_BENCHMARK_SUCCESS, value } + {type: Types.CREATE_BENCHMARK_SUCCESS, value} ); export const createBenchmarkFailure = (error) => ( - { type: Types.CREATE_BENCHMARK_FAILURE, error } + {type: Types.CREATE_BENCHMARK_FAILURE, error} ); export const getAggregateReportSuccess = (data) => ( - { type: Types.GET_AGGREGATE_REPORT_SUCCESS, data } + {type: Types.GET_AGGREGATE_REPORTS_SUCCESS, data} ); export const editReport = (testId, reportId, body) => ( - {type: Types.EDIT_REPORT, testId,reportId, body} + {type: Types.EDIT_REPORT, testId, reportId, body} ); export const editReportSuccess = (value) => ( @@ -82,4 +82,10 @@ export const cleanAllReportsErrors = () => ( {type: Types.CLEAN_ALL_ERRORS} ); +export const addReportForCompare = (testId, reportId, value) => ( + {type: Types.ADD_REPORT_FOR_COMPARE, testId, reportId, value} +); +export const clearReportForCompare = () => ( + {type: Types.CLEAR_REPORT_FOR_COMPARE } +); diff --git a/ui/src/features/redux/reducers/reportsReducer.js b/ui/src/features/redux/reducers/reportsReducer.js index 917d96b02..8daaad7ee 100644 --- a/ui/src/features/redux/reducers/reportsReducer.js +++ b/ui/src/features/redux/reducers/reportsReducer.js @@ -10,7 +10,9 @@ const initialState = Immutable.Map({ create_benchmark_success: false, edit_notes_success: false, edit_report_failure: undefined, - create_benchmark_failure: undefined + create_benchmark_failure: undefined, + selected_reports: {}, + aggregate_reports: [] }); export default function reduce(state = initialState, action = {}) { @@ -31,8 +33,8 @@ export default function reduce(state = initialState, action = {}) { return state.set('processing_get_reports', action.state); case Types.CLEAR_SELECTED_REPORT: return state.set('report', undefined); - case Types.GET_AGGREGATE_REPORT_SUCCESS: - return state.set('aggregate_report', action.data); + case Types.GET_AGGREGATE_REPORTS_SUCCESS: + return state.set('aggregate_reports', action.data); case Types.CREATE_BENCHMARK_SUCCESS: return state.set('create_benchmark_success', action.value); case Types.EDIT_REPORT_SUCCESS: @@ -41,6 +43,13 @@ export default function reduce(state = initialState, action = {}) { return state.set('edit_report_failure', action.error); case Types.CREATE_BENCHMARK_FAILURE: return state.set('create_benchmark_failure', action.error); + case Types.ADD_REPORT_FOR_COMPARE: + const currentSelectedReports = JSON.parse(JSON.stringify(state.get('selected_reports'))); + currentSelectedReports[action.testId] = currentSelectedReports[action.testId] || {}; + currentSelectedReports[action.testId][action.reportId] = action.value; + return state.set('selected_reports', currentSelectedReports); + case Types.CLEAR_REPORT_FOR_COMPARE: + return state.set('selected_reports', {}); case Types.CLEAN_ALL_ERRORS: const newState = (state.set('error_get_reports', undefined) .set('error_get_reports', undefined) diff --git a/ui/src/features/redux/saga/reportsSagas.js b/ui/src/features/redux/saga/reportsSagas.js index dd16d9b38..2ecb55b7a 100644 --- a/ui/src/features/redux/saga/reportsSagas.js +++ b/ui/src/features/redux/saga/reportsSagas.js @@ -1,4 +1,4 @@ -import {put, takeLatest, select, call} from 'redux-saga/effects' +import {put, takeLatest, select, all, call} from 'redux-saga/effects' import * as Actions from '../actions/reportsActions' import * as Types from '../types/reportsTypes' import { @@ -73,19 +73,24 @@ export function* createBenchmark({testId, body}) { yield put(Actions.createBenchmarkFailure(e)) } } -export function* editReport({testId,reportId, body}) { + +export function* editReport({testId, reportId, body}) { try { - yield call(editReportFromFramework, testId,reportId, body); + yield call(editReportFromFramework, testId, reportId, body); yield put(Actions.editReportSuccess(true)); } catch (e) { yield put(Actions.editReportFailure(e)) } } -export function* getAggregateReport({testId, reportId}) { +export function* getAggregateReports({reportsData}) { try { - const report = yield call(getAggregateFromFramework, testId, reportId); - yield put(Actions.getAggregateReportSuccess(report.data)); + const results = yield all(reportsData.map(report => { + return call(getAggregateFromFramework, report.testId, report.reportId) + })); + + const data = results.map((result) => result.data); + yield put(Actions.getAggregateReportSuccess(data)); } catch (e) { console.log('error', e); //TODO @@ -97,7 +102,7 @@ export function* reportsRegister() { yield takeLatest(Types.GET_REPORTS, getReports); yield takeLatest(Types.GET_REPORT, getReport); yield takeLatest(Types.GET_LAST_REPORTS, getLastReports); - yield takeLatest(Types.GET_AGGREGATE_REPORT, getAggregateReport); + yield takeLatest(Types.GET_AGGREGATE_REPORTS, getAggregateReports); yield takeLatest(Types.CREATE_BENCHMARK, createBenchmark); yield takeLatest(Types.EDIT_REPORT, editReport); } diff --git a/ui/src/features/redux/selectors/reportsSelector.js b/ui/src/features/redux/selectors/reportsSelector.js index 54213366f..444a70f13 100644 --- a/ui/src/features/redux/selectors/reportsSelector.js +++ b/ui/src/features/redux/selectors/reportsSelector.js @@ -1,8 +1,8 @@ import dateFormat from "dateformat"; -import { createSelector } from 'reselect' +import {createSelector} from 'reselect' export const reports = (state) => state.ReportsReducer.get('reports'); -export const aggregateReport = (state) => state.ReportsReducer.get('aggregate_report'); +export const aggregateReport = (state) => state.ReportsReducer.get('aggregate_reports'); export const errorOnGetReports = (state) => state.ReportsReducer.get('error_get_reports'); export const report = (state) => state.ReportsReducer.get('report'); export const errorOnGetReport = (state) => state.ReportsReducer.get('error_get_report'); @@ -11,69 +11,118 @@ export const createBenchmarkSuccess = (state) => state.ReportsReducer.get('creat export const editNotesSuccess = (state) => state.ReportsReducer.get('edit_notes_success'); export const createBenchmarkFailure = (state) => state.ReportsReducer.get('create_benchmark_failure'); export const editReportFailure = (state) => state.ReportsReducer.get('edit_report_failure'); +export const selectedReports = (state) => state.ReportsReducer.get('selected_reports'); -export const getAggregateReport = createSelector(aggregateReport,(report)=>{ - const latencyGraph = [], - errorsCodeGraph = [], - errorCodes = {}, - errorsGraph = [], - errors = {}, - rps = [], - errorsBar = [], - scenarios = [], - benchMark = {}; - if (report) { - const startTime = new Date(report.start_time).getTime(); +export const isAtLeastOneReportSelected = createSelector(selectedReports, (selectedReports) => { + //find report with value true + const result = Object.values(selectedReports).find((value) => (Object.values(value).find((value) => value))); + return !!result; +}); + +export const getAggregateReport = createSelector(aggregateReport, (reports) => { + return buildAggregateReportData(reports)[0] || {}; +}); + +export const getAggregateReportsForCompare = createSelector(aggregateReport, (reports) => { + return buildAggregateReportData(reports, true, true); +}); + + +function buildAggregateReportData(reports, withPrefix, startFromZeroTime) { + let prefix = withPrefix ? 'A_' : ''; + + return reports.map((report) => { + const latencyGraph = [], + errorsCodeGraph = [], + errorCodes = {}, + errorsGraph = [], + errors = {}, + rps = [], + errorsBar = [], + scenarios = [], + benchMark = {}; + let errorsCodeGraphKeysAsObjectAcc = {}; + + const offset = startFromZeroTime ? new Date(report.start_time).getTime() : 0; + + const startTime = new Date(report.start_time).getTime() - offset; report.intermediates.forEach((bucket, index) => { const latency = bucket.latency; const time = new Date(startTime + (bucket.bucket * 1000)); + const timeMills = time.getTime(); + latencyGraph.push({ name: `${dateFormat(time, 'h:MM:ss')}`, - median: latency.median, - p95: latency.p95, - p99: latency.p99, + [`${prefix}median`]: latency.median, + [`${prefix}p95`]: latency.p95, + [`${prefix}p99`]: latency.p99, + timeMills }); - rps.push({name: `${dateFormat(time, 'h:MM:ss')}`, mean: bucket.rps.mean}); + rps.push({name: `${dateFormat(time, 'h:MM:ss')}`, timeMills, [`${prefix}mean`]: bucket.rps.mean}); if (Object.keys(bucket.codes).length > 0) { - errorsCodeGraph.push({name: `${dateFormat(time, 'h:MM:ss')}`, ...bucket.codes, ...bucket.errors}); + const errorsData = Object.entries({...bucket.codes, ...bucket.errors}).reduce((acc, cur) => { + acc[`${prefix}${cur[0]}`] = cur[1]; + return acc; + }, {}); + errorsCodeGraphKeysAsObjectAcc = Object.assign(errorsCodeGraphKeysAsObjectAcc, errorsData) + errorsCodeGraph.push({name: `${dateFormat(time, 'h:MM:ss')}`, timeMills, ...errorsData}); Object.keys(bucket.codes).forEach((code) => { - errorCodes[code] = true; + errorCodes[`${prefix}${code}`] = true; }); Object.keys(bucket.errors).forEach((error) => { - errorCodes[error] = true; + errorCodes[`${prefix}${error}`] = true; }) } }); Object.keys(report.aggregate.codes).forEach((code) => { - errorsBar.push({name: code, count: report.aggregate.codes[code]}) + errorsBar.push({name: code, [`${prefix}count`]: report.aggregate.codes[code]}) }); Object.keys(report.aggregate.errors).forEach((error) => { - errorsBar.push({name: error, count: report.aggregate.errors[error]}) + errorsBar.push({name: error, [`${prefix}count`]: report.aggregate.errors[error]}) }); Object.keys(report.aggregate.scenarioCounts).forEach((key) => { - scenarios.push({name: key, value: report.aggregate.scenarioCounts[key]}) - }) + scenarios.push({name: `${prefix}${key}`, value: report.aggregate.scenarioCounts[key]}) + }); - if(report.aggregate){ + if (report.aggregate) { benchMark.rps = report.aggregate.rps; benchMark.latency = report.aggregate.latency; benchMark.errors = report.aggregate.errors; benchMark.codes = report.aggregate.codes; - } - } - - return { - latencyGraph, - errorsCodeGraph, - errorCodes, - errorsGraph, - errors, - rps, - errorsBar, - scenarios, - benchMark - } -}); + } + const alias = prefix.substring(0, 1); + + const latencyGraphKeys = [`${prefix}median`, `${prefix}p95`, `${prefix}p99`]; + const rpsKeys = [`${prefix}mean`]; + const errorsBarKeys = [`${prefix}count`]; + + + const errorsCodeGraphKeys = Object.keys(errorsCodeGraphKeysAsObjectAcc) + + if (withPrefix) { + prefix = String.fromCharCode(prefix.charCodeAt(0) + 1) + '_'; + } + return { + alias, + latencyGraph, + latencyGraphKeys, + errorsCodeGraph, + errorsCodeGraphKeys, + errorCodes, + errorsGraph, + errors, + rps, + rpsKeys, + errorsBar, + errorsBarKeys, + scenarios, + benchMark, + startTime: report.start_time, + testName: report.test_name, + duration: report.duration, + } + }) +} diff --git a/ui/src/features/redux/types/reportsTypes.js b/ui/src/features/redux/types/reportsTypes.js index 7c96cd5b3..2ec2f319e 100644 --- a/ui/src/features/redux/types/reportsTypes.js +++ b/ui/src/features/redux/types/reportsTypes.js @@ -12,9 +12,9 @@ export const CLEAR_SELECTED_REPORT = 'CLEAR_SELECTED_REPORT'; export const CLEAR_REPORTS = 'CLEAR_REPORTS'; export const PROCESSING_GET_REPORTS = 'PROCESSING_GET_REPORTS'; -export const GET_AGGREGATE_REPORT = 'GET_AGGREGATE_REPORT'; -export const GET_AGGREGATE_REPORT_SUCCESS = 'GET_AGGREGATE_REPORT_SUCCESS'; -export const GET_AGGREGATE_REPORT_FAILURE = 'GET_AGGREGATE_REPORT_FAILURE'; +export const GET_AGGREGATE_REPORTS = 'GET_AGGREGATE_REPORTS'; +export const GET_AGGREGATE_REPORTS_SUCCESS = 'GET_AGGREGATE_REPORTS_SUCCESS'; +export const GET_AGGREGATE_REPORTS_FAILURE = 'GET_AGGREGATE_REPORTS_FAILURE'; export const CREATE_BENCHMARK = 'CREATE_BENCHMARK'; @@ -30,3 +30,7 @@ export const EDIT_REPORT_FAILURE = 'EDIT_REPORT_FAILURE'; export const CLEAN_ALL_ERRORS = 'CLEAN_ALL_ERRORS'; +export const ADD_REPORT_FOR_COMPARE = 'ADD_REPORT_FOR_COMPARE'; +export const CLEAR_REPORT_FOR_COMPARE = 'CLEAR_REPORT_FOR_COMPARE'; + +