From e3409799650831a9632ae1cc51e00c3e544f87ef Mon Sep 17 00:00:00 2001 From: manorlh <44364426+manorlh@users.noreply.github.com> Date: Thu, 16 May 2019 18:13:33 +0300 Subject: [PATCH] feat: rerun feature, search by enter (#137) * feat: rerun feature,search-enter --- ui/src/components/SearchBar/index.js | 8 ++- .../PerformanceUI/components/JobForm/index.js | 28 +++----- .../instance/configurationColumn.js | 64 ++++++++++++++++--- .../PerformanceUI/instance/get-jobs.js | 59 +++++++++++------ .../instance/get-last-reports.js | 58 ++++++++++++----- .../instance/get-test-reports.js | 62 ++++++++++++++---- .../PerformanceUI/instance/get-tests.js | 4 -- .../PerformanceUI/instance/redux/action.js | 3 +- .../instance/redux/actions/jobsActions.js | 4 -- .../instance/redux/reducers/jobsReducer.js | 2 - .../instance/redux/selectors/jobsSelector.js | 1 - .../PerformanceUI/instance/requestBuilder.js | 21 ++++++ 12 files changed, 222 insertions(+), 92 deletions(-) create mode 100644 ui/src/features/PerformanceUI/instance/requestBuilder.js diff --git a/ui/src/components/SearchBar/index.js b/ui/src/components/SearchBar/index.js index 7a13abcc9..cb490fb42 100644 --- a/ui/src/components/SearchBar/index.js +++ b/ui/src/components/SearchBar/index.js @@ -17,13 +17,19 @@ class SearchBar extends React.Component {
- this.setState({value: event.target.value})}/> + this.setState({value: event.target.value})}/>
) } + + handleEnter=(e)=>{ + if (e.key === 'Enter') { + this.props.onSearch(this.state.value) + } + } } export default SearchBar; diff --git a/ui/src/features/PerformanceUI/components/JobForm/index.js b/ui/src/features/PerformanceUI/components/JobForm/index.js index e80c4328d..b6aac43fd 100644 --- a/ui/src/features/PerformanceUI/components/JobForm/index.js +++ b/ui/src/features/PerformanceUI/components/JobForm/index.js @@ -21,6 +21,7 @@ import TextArea from '../../../../components/TextArea'; import MultiValueInput from '../../../../components/MultiValueInput'; import UiSwitcher from '../../../../components/UiSwitcher'; import {filter} from 'lodash'; +import {createJobRequest} from '../../instance/requestBuilder'; const DESCRIPTION = 'Predator executes tests through jobs. Use this form to specify the parameters for the job you want to execute.'; const inputTypes = { @@ -228,7 +229,7 @@ class Form extends React.Component { } render() { - const {closeDialog, processingAction} = this.props; + const {closeDialog, processingAction, serverError,clearErrorOnCreateJob} = this.props; return ( @@ -248,7 +249,10 @@ class Form extends React.Component { - {this.props.serverError ? : null} + { serverError && + {clearErrorOnCreateJob()}} showMessage={serverError}/> + } + @@ -300,7 +304,6 @@ class Form extends React.Component { ); default: - console.log('oneItem', oneItem) return (
{ - - let body = { + this.props.createJob(createJobRequest(Object.assign({}, this.state, { test_id: this.props.data.id, - arrival_rate: parseInt(this.state.arrival_rate), duration: parseInt(this.state.duration) * 60, - ramp_to: this.state.ramp_to ? parseInt(this.state.ramp_to) : undefined, - environment: this.state.environment, - run_immediately: (this.state.run_immediately === undefined) ? false : this.state.run_immediately, - emails: this.state.emails, - webhooks: this.state.webhooks, - notes: this.state.notes, - parallelism: this.state.parallelism ? parseInt(this.state.parallelism) : undefined, - max_virtual_users: this.state.max_virtual_users ? parseInt(this.state.max_virtual_users) : undefined - }; - if (this.state.cron_expression) {//should exist and not empty - body.cron_expression = this.state.cron_expression - } - body = JSON.parse(JSON.stringify(body)); - this.props.createJob(body); + }))); }; } diff --git a/ui/src/features/PerformanceUI/instance/configurationColumn.js b/ui/src/features/PerformanceUI/instance/configurationColumn.js index 985b8b0aa..16b000f3b 100644 --- a/ui/src/features/PerformanceUI/instance/configurationColumn.js +++ b/ui/src/features/PerformanceUI/instance/configurationColumn.js @@ -5,7 +5,7 @@ import Moment from 'moment'; import prettySeconds from 'pretty-seconds'; import 'font-awesome/css/font-awesome.min.css'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faEye, faCloudDownloadAlt, faStopCircle, faTrashAlt, faPen} from '@fortawesome/free-solid-svg-icons' +import {faEye,faRedo, faRunning, faCloudDownloadAlt, faStopCircle, faTrashAlt, faPen} from '@fortawesome/free-solid-svg-icons' import classnames from 'classnames'; import css from './configurationColumn.scss'; import env from "../../../App/common/env"; @@ -14,7 +14,7 @@ import TooltipWrapper from '../../../components/TooltipWrapper'; import {getTimeFromCronExpr} from './utils'; -export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, onRawView,onStop, onDelete, onEdit, onRunTest }) => { +export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, onRawView, onStop, onDelete, onEdit, onRunTest}) => { const columns = [ { @@ -69,7 +69,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, Edit ), - accessor: data => data.type ==='basic' ? onEdit(data)}/> : 'N/A', + accessor: data => data.type ==='basic' ? { + e.stopPropagation(); + onEdit(data)}}/> : 'N/A', className: css['small-header'], headerClassName: css['small-header'] }, @@ -226,7 +228,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, Report ), - accessor: data => onReportView(data)}/>, + accessor: data => { + e.stopPropagation(); + onReportView(data)}}/>, className: css['small-header'], headerClassName: css['small-header'] }, @@ -237,7 +241,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, Grafana ), - accessor: data => window.open(data.grafana_report, '_blank')}/>, + accessor: data => { + e.stopPropagation(); + window.open(data.grafana_report, '_blank')}}/>, className: css['small-header'], headerClassName: css['small-header'] }, @@ -248,7 +254,37 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, Raw ), - accessor: data => onRawView(data)}/>, + accessor: data => { + e.stopPropagation(); + onRawView(data)}}/>, + className: css['small-header'], + headerClassName: css['small-header'], + + }, + { + id: 'rerun', + Header: () => ( + + Rerun + + ), + accessor: data => { + e.stopPropagation(); + onRunTest(data)}}/>, + className: css['small-header'], + headerClassName: css['small-header'], + + }, + { + id: 'run_now', + Header: () => ( + + Run Now + + ), + accessor: data => { + e.stopPropagation(); + onRunTest(data)}}/>, className: css['small-header'], headerClassName: css['small-header'], @@ -260,7 +296,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, Delete ), - accessor: data => onDelete(data)}/>, + accessor: data => { + e.stopPropagation(); + onDelete(data)}}/>, className: css['small-header'], headerClassName: css['small-header'] }, @@ -271,7 +309,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, Run Test ), - accessor: data => onRunTest(data)}/>, + accessor: data => { + e.stopPropagation(); + onRunTest(data)}}/>, className: css['small-header'], headerClassName: css['small-header'] }, { @@ -282,7 +322,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, ), accessor: data => ( window.open(`${env.PREDATOR_URL}/jobs/${data.job_id}/runs/${data.report_id}/logs`, '_blank')}/>), + onClick={(e) => { + 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'] }, { @@ -294,7 +336,9 @@ export const getColumns = ({columnsNames, sortHeader = '', onSort, onReportView, ), accessor: (data) => { const disabled = (data.status !== 'in_progress' && data.status !== 'started'); - return ( onStop(data)}/>) + return ( { + e.stopPropagation(); + onStop(data)}}/>) }, className: css['small-header'], headerClassName: css['small-header'] diff --git a/ui/src/features/PerformanceUI/instance/get-jobs.js b/ui/src/features/PerformanceUI/instance/get-jobs.js index aa045fdfb..4e611001b 100644 --- a/ui/src/features/PerformanceUI/instance/get-jobs.js +++ b/ui/src/features/PerformanceUI/instance/get-jobs.js @@ -5,10 +5,10 @@ import { job, processingGetJobs, errorOnGetJobs, - errorOnGetJob, processingDeleteJob, deleteJobSuccess, - getJobsWithTestNameAndLastRun + getJobsWithTestNameAndLastRun, + createJobSuccess } from './redux/selectors/jobsSelector'; import { tests } from './redux/selectors/testsSelector'; import { reports } from './redux/selectors/reportsSelector'; @@ -21,11 +21,12 @@ import Page from '../../../components/Page'; import { ReactTableComponent } from '../../../components/ReactTable'; import { getColumns } from './configurationColumn'; import _ from 'lodash'; +import {createJobRequest} from './requestBuilder'; const noDataMsg = 'There is no data to display.'; const errorMsgGetTests = 'Error occurred while trying to get all jobs.'; const REFRESH_DATA_INTERVAL = 30000; -const columnsNames = ['test_name', 'environment', 'duration', 'arrival_rate', 'ramp_to', 'parallelism', 'max_virtual_users', 'cron_expression', 'last_run', 'raw', 'delete']; +const columnsNames = ['test_name', 'environment', 'duration', 'arrival_rate', 'ramp_to', 'parallelism', 'max_virtual_users', 'cron_expression', 'last_run', 'run_now', 'raw', 'delete']; const DESCRIPTION = 'Scheduled jobs configured with a cron expression.'; class getJobs extends React.Component { @@ -37,7 +38,8 @@ class getJobs extends React.Component { deleteDialog: false, openViewJob: false, jobToDelete: undefined, - sortedJobs: [] + sortedJobs: [], + rerunJob: null }; } @@ -69,6 +71,14 @@ class getJobs extends React.Component { this.setState({ openViewJob: job }); }; + onRunTest = (job) => { + const request = createJobRequest(job); + delete request.cron_expression; + request.run_immediately = true; + this.props.createJob(request); + this.setState({rerunJob:job}); + }; + submitDelete = () => { this.props.deleteJob(this.state.jobToDelete.id); this.props.getAllJobs(); @@ -127,9 +137,10 @@ class getJobs extends React.Component { columnsNames, onSort: this.onSort, onRawView: this.onRawView, + onRunTest: this.onRunTest, onDelete: this.onDelete }); - + const feedbackMessage = this.generateFeedbackMessage(); return ( : null} - {/* //TODO seems deleteError will not work */} - { this.props.getAllJobs(); this.props.clearDeleteJobSuccess(); + this.props.createJobSuccess(undefined); this.setState({ - jobToDelete: undefined + jobToDelete: undefined, + rerunJob: null }); }} - /> + />} ); } + + generateFeedbackMessage = ()=>{ + if(this.state.jobToDelete && this.state.jobToDelete.id){ + return `Job deleted successfully: ${this.state.jobToDelete.id}` + } + if(this.props.jobSuccess && this.state.rerunJob){ + return `Job created successfully: ${this.props.jobSuccess.id}`; + } + + } } function mapStateToProps(state) { @@ -184,11 +202,12 @@ function mapStateToProps(state) { job: job(state), processingGetJobs: processingGetJobs(state), errorOnGetJobs: errorOnGetJobs(state), - errorOnGetJob: errorOnGetJob(state), processingDeleteJob: processingDeleteJob(state), deleteJobSuccess: deleteJobSuccess(state), tests: tests(state), - reports: reports(state) + reports: reports(state), + jobSuccess: createJobSuccess(state) + }; } @@ -197,11 +216,13 @@ const mapDispatchToProps = { clearErrorOnGetJobs: Actions.clearErrorOnGetJobs, getAllJobs: Actions.getJobs, getJob: Actions.getJob, + createJob: Actions.createJob, deleteJob: Actions.deleteJob, clearDeleteJobSuccess: Actions.clearDeleteJobSuccess, clearErrorOnDeleteJob: Actions.clearErrorOnDeleteJob, getTests: Actions.getTests, - getAllReports: Actions.getLastReports + getAllReports: Actions.getLastReports, + createJobSuccess: Actions.createJobSuccess, }; export default connect(mapStateToProps, mapDispatchToProps)(getJobs); diff --git a/ui/src/features/PerformanceUI/instance/get-last-reports.js b/ui/src/features/PerformanceUI/instance/get-last-reports.js index f77743818..4f2d1019c 100644 --- a/ui/src/features/PerformanceUI/instance/get-last-reports.js +++ b/ui/src/features/PerformanceUI/instance/get-last-reports.js @@ -1,7 +1,7 @@ import React from 'react'; import {connect} from 'react-redux'; import * as selectors from './redux/selectors/reportsSelector'; -import {errorOnStopRunningJob, stopRunningJobSuccess} from './redux/selectors/jobsSelector'; +import {createJobSuccess, errorOnStopRunningJob, stopRunningJobSuccess} from './redux/selectors/jobsSelector'; import {tests} from './redux/selectors/testsSelector'; import Snackbar from 'material-ui/Snackbar'; import style from './style.scss'; @@ -10,6 +10,7 @@ import * as Actions from './redux/action'; import Page from '../../../components/Page'; import _ from 'lodash'; import Report from '../components/Report'; +import {createJobRequest} from './requestBuilder'; import {ReactTableComponent} from './../../../components/ReactTable'; import {getColumns} from './configurationColumn' @@ -17,8 +18,8 @@ import {getColumns} from './configurationColumn' 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', 'report', 'grafana_report', '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.'; + 'ramp_to', 'last_success_rate', 'last_rps', 'parallelism', 'notes', '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 { constructor(props) { @@ -27,7 +28,9 @@ class getReports extends React.Component { this.state = { showReport: false, sortedReports: [], - sortHeader: '' + sortHeader: '', + rerunJob: null + }; } @@ -42,6 +45,13 @@ class getReports extends React.Component { this.setState({openViewReport: report}); }; + onRunTest = (job) => { + const requestBody = createJobRequest(job); + delete requestBody.cron_expression; + requestBody.run_immediately = true; + this.props.createJob(requestBody); + this.setState({rerunJob: job}); + }; closeViewReportDialog = () => { this.setState({ @@ -114,9 +124,10 @@ class getReports extends React.Component { onSort: this.onSort, onReportView: this.onReportView, onRawView: this.onRawView, - onStop: this.onStop + onStop: this.onStop, + onRunTest: this.onRunTest }); - + const feedbackMessage = this.generateFeedbackMessage(); return (
@@ -139,27 +150,37 @@ class getReports extends React.Component { ? : null} - { this.props.getAllReports(); this.props.clearStopJobSuccess(); this.props.clearStoppedJobError(); + this.props.createJobSuccess(undefined); this.setState({ - showSnackbar: false + showSnackbar: false, + rerunJob: null }); }} - /> + />} ) } + + generateFeedbackMessage = () => { + if (this.props.stopRunningJobSuccess) { + return 'Job successfully aborted' + } + if (this.props.jobSuccess && this.state.rerunJob) { + return `Job created successfully: ${this.props.jobSuccess.id}`; + } + + } + + } function mapStateToProps(state) { @@ -171,7 +192,8 @@ function mapStateToProps(state) { errorOnGetReport: selectors.errorOnGetReport(state), errorOnStopRunningJob: errorOnStopRunningJob(state), stopRunningJobSuccess: stopRunningJobSuccess(state), - tests: tests(state) + tests: tests(state), + jobSuccess: createJobSuccess(state) } } @@ -183,7 +205,9 @@ const mapDispatchToProps = { stopRunningJob: Actions.stopRunningJob, clearStopJobSuccess: Actions.clearStopJobSuccess, clearStoppedJobError: Actions.clearErrorOnStopJob, - getTests: Actions.getTests + createJob: Actions.createJob, + getTests: Actions.getTests, + createJobSuccess: Actions.createJobSuccess }; export default connect(mapStateToProps, mapDispatchToProps)(getReports); diff --git a/ui/src/features/PerformanceUI/instance/get-test-reports.js b/ui/src/features/PerformanceUI/instance/get-test-reports.js index be617047b..053c62d5d 100644 --- a/ui/src/features/PerformanceUI/instance/get-test-reports.js +++ b/ui/src/features/PerformanceUI/instance/get-test-reports.js @@ -16,11 +16,14 @@ import _ from 'lodash'; import Report from "../components/Report"; import {ReactTableComponent} from "../../../components/ReactTable"; import {getColumns} from "./configurationColumn"; +import {createJobRequest} from "./requestBuilder"; +import {createJobSuccess} from "./redux/selectors/jobsSelector"; +import Snackbar from 'material-ui/Snackbar'; const noDataMsg = 'There is no data to display.'; const errorMsgGetReports = 'Error occurred while trying to get all reports for test.'; -const columnsNames = ['start_time', 'end_time', 'duration', 'status','arrival_rate', - 'ramp_to', 'last_success_rate', 'last_rps', 'parallelism', 'notes', 'grafana_report', 'report', 'raw', 'logs']; +const columnsNames = ['start_time', 'end_time', 'duration', 'status', 'arrival_rate', + 'ramp_to', 'last_success_rate', 'last_rps', 'parallelism', 'notes', 'grafana_report', 'report', 'rerun', 'raw', 'logs']; const DESCRIPTION = 'All reports for a given test.'; @@ -35,11 +38,19 @@ class getTests extends React.Component { openSnakeBar: false, openViewReport: false, showReport: null, - sortedReports:[], - sortHeader:'' + sortedReports: [], + sortHeader: '', + rerunJob: null }; } + onRunTest = (job) => { + const request = createJobRequest(job); + delete request.cron_expression; + request.run_immediately = true; + this.props.createJob(request); + this.setState({rerunJob: job}); + }; componentDidUpdate(prevProps) { if (prevProps.reports !== this.props.reports) { @@ -74,10 +85,10 @@ class getTests extends React.Component { }; - onReportView=(data)=>{ + onReportView = (data) => { this.setState({showReport: data}) }; - onRawView=(data)=>{ + onRawView = (data) => { this.setState({openViewReport: data}); }; @@ -117,13 +128,15 @@ class getTests extends React.Component { onSort: this.onSort, onReportView: this.onReportView, onRawView: this.onRawView, - onStop: this.onStop + onStop: this.onStop, + onRunTest: this.onRunTest }); const {showReport} = this.state; + const feedbackMessage = this.generateFeedbackMessage(); return ( 0 && `${this.props.reports[0].test_name} Reports`} - description={DESCRIPTION}> + description={DESCRIPTION}> - {showReport && } - {this.state.openViewReport ? : null} + {showReport && + } + {this.state.openViewReport ? : null} + {feedbackMessage && { + this.props.createJobSuccess(undefined); + this.setState({ + rerunJob: null + }); + }} + />} ) } + + generateFeedbackMessage = () => { + + if (this.props.jobSuccess && this.state.rerunJob) { + return `Job created successfully: ${this.props.jobSuccess.id}`; + } + + } } function mapStateToProps(state) { @@ -150,7 +185,8 @@ function mapStateToProps(state) { report: report(state), processingGetReports: processingGetReports(state), errorOnGetReports: errorOnGetReports(state), - errorOnGetReport: errorOnGetReport(state) + errorOnGetReport: errorOnGetReport(state), + jobSuccess: createJobSuccess(state) } } @@ -159,7 +195,9 @@ const mapDispatchToProps = { clearSelectedTest: Actions.clearSelectedTest, clearErrorOnGetReports: Actions.clearErrorOnGetReports, getReports: Actions.getReports, - getReport: Actions.getReport + getReport: Actions.getReport, + createJob: Actions.createJob, + createJobSuccess: Actions.createJobSuccess }; export default connect(mapStateToProps, mapDispatchToProps)(getTests); \ No newline at end of file diff --git a/ui/src/features/PerformanceUI/instance/get-tests.js b/ui/src/features/PerformanceUI/instance/get-tests.js index cf57c9998..1062d2481 100644 --- a/ui/src/features/PerformanceUI/instance/get-tests.js +++ b/ui/src/features/PerformanceUI/instance/get-tests.js @@ -219,10 +219,6 @@ class getTests extends React.Component { fix using redux to be with less variables. */} ( { type: Types.CLEAR_ERROR_ON_GET_JOBS } ); -export const clearJobs = () => ( - { type: Types.CLEAR_JOBS } -); - export const getJob = (jobId) => ( { type: Types.GET_JOB, jobId } ); diff --git a/ui/src/features/PerformanceUI/instance/redux/reducers/jobsReducer.js b/ui/src/features/PerformanceUI/instance/redux/reducers/jobsReducer.js index 42595de99..2194fe1e3 100644 --- a/ui/src/features/PerformanceUI/instance/redux/reducers/jobsReducer.js +++ b/ui/src/features/PerformanceUI/instance/redux/reducers/jobsReducer.js @@ -21,8 +21,6 @@ export default function reduce (state = initialState, action = {}) { return state.set('error_get_jobs', action.error); case Types.GET_JOBS_SUCCESS: return state.set('jobs', action.jobs); - case Types.CLEAR_JOBS: - return state.set('jobs', []); case Types.CLEAR_ERROR_ON_GET_JOBS: return state.set('error_get_jobs', undefined); case Types.GET_JOB_FAILURE: diff --git a/ui/src/features/PerformanceUI/instance/redux/selectors/jobsSelector.js b/ui/src/features/PerformanceUI/instance/redux/selectors/jobsSelector.js index b73268023..dd6f8c338 100644 --- a/ui/src/features/PerformanceUI/instance/redux/selectors/jobsSelector.js +++ b/ui/src/features/PerformanceUI/instance/redux/selectors/jobsSelector.js @@ -4,7 +4,6 @@ import {reports} from './reportsSelector'; export const jobs = (state) => state.JobsReducer.get('jobs'); export const errorOnGetJobs = (state) => state.JobsReducer.get('error_get_jobs'); export const job = (state) => state.JobsReducer.get('job'); -export const errorOnGetJob = (state) => state.JobsReducer.get('error_get_job'); export const processingGetJobs = (state) => state.JobsReducer.get('processing_get_jobs'); export const processingCreateJob = (state) => state.JobsReducer.get('processing_create_job'); export const errorOnNewJob = (state) => state.JobsReducer.get('error_new_job'); diff --git a/ui/src/features/PerformanceUI/instance/requestBuilder.js b/ui/src/features/PerformanceUI/instance/requestBuilder.js new file mode 100644 index 000000000..c8e979d1b --- /dev/null +++ b/ui/src/features/PerformanceUI/instance/requestBuilder.js @@ -0,0 +1,21 @@ + +export const createJobRequest = (opts) => { + let body = { + test_id: opts.test_id, + arrival_rate: parseInt(opts.arrival_rate), + duration: parseInt(opts.duration), + ramp_to: opts.ramp_to ? parseInt(opts.ramp_to) : undefined, + environment: opts.environment, + run_immediately: (opts.run_immediately === undefined) ? false : opts.run_immediately, + emails: opts.emails, + webhooks: opts.webhooks, + notes: opts.notes, + parallelism: opts.parallelism ? parseInt(opts.parallelism) : undefined, + max_virtual_users: opts.max_virtual_users ? parseInt(opts.max_virtual_users) : undefined + }; + if (opts.cron_expression) {//should exist and not empty + body.cron_expression = opts.cron_expression + } + body = JSON.parse(JSON.stringify(body)); + return body; +}; \ No newline at end of file