From 03fffe4a057da647cb70d4ac25ac3c2b5524bcf4 Mon Sep 17 00:00:00 2001 From: kerenfinkelstein Date: Mon, 18 Sep 2023 14:54:04 +0300 Subject: [PATCH 1/6] feat: add chaos experiments ui page with crud integration --- ui/src/App/index.js | 84 +- ui/src/App/rootSagas.js | 2 + ui/src/components/Dropdown/CustomDropdown.js | 23 + .../components/ChaosExperimentForm/index.js | 257 ++++ .../components/ChaosExperimentForm/style.scss | 52 + .../components/TestForm/CustomDropdown.js | 24 - .../TestForm/DynamicKeyValueInput.js | 110 +- .../features/components/TestForm/StepForm.js | 2 +- .../features/components/TestForm/constants.js | 136 +- ui/src/features/configurationColumn.js | 1153 +++++++++-------- ui/src/features/get-chaos-experiments.js | 207 +++ ui/src/features/get-processors.js | 274 ++-- ui/src/features/mainMenu.js | 127 +- ui/src/features/redux/action.js | 384 +++--- .../redux/actions/chaosExperimentsActions.js | 39 + .../redux/apis/chaosExperimentsApi.js | 20 + .../redux/reducers/chaosExperimentsReducer.js | 38 + .../redux/saga/chaosExperimentsSagas.js | 45 + .../selectors/chaosExperimentsSelector.js | 6 + .../redux/types/chaosExperimentsTypes.js | 15 + ui/src/store/reducers.js | 16 +- 21 files changed, 1866 insertions(+), 1148 deletions(-) create mode 100644 ui/src/components/Dropdown/CustomDropdown.js create mode 100644 ui/src/features/components/ChaosExperimentForm/index.js create mode 100644 ui/src/features/components/ChaosExperimentForm/style.scss delete mode 100644 ui/src/features/components/TestForm/CustomDropdown.js create mode 100644 ui/src/features/get-chaos-experiments.js create mode 100644 ui/src/features/redux/actions/chaosExperimentsActions.js create mode 100644 ui/src/features/redux/apis/chaosExperimentsApi.js create mode 100644 ui/src/features/redux/reducers/chaosExperimentsReducer.js create mode 100644 ui/src/features/redux/saga/chaosExperimentsSagas.js create mode 100644 ui/src/features/redux/selectors/chaosExperimentsSelector.js create mode 100644 ui/src/features/redux/types/chaosExperimentsTypes.js diff --git a/ui/src/App/index.js b/ui/src/App/index.js index e35c1121d..863dcd331 100644 --- a/ui/src/App/index.js +++ b/ui/src/App/index.js @@ -1,6 +1,7 @@ import React, { Fragment } from 'react'; import GetTests from '../features/get-tests'; import GetProcessors from '../features/get-processors'; +import GetChaosExperiments from '../features/get-chaos-experiments'; import GetJobs from '../features/get-jobs'; import GetReports from '../features/get-last-reports'; import GetTestReports from '../features/get-test-reports'; @@ -29,53 +30,56 @@ class App extends React.Component { render () { return ( - - - ( - - )} /> - ( - - )} /> - ( - - )} /> - ( - - )} /> - ( - - )} /> - ( - - )} /> - ( - - )} /> - ( - - )} /> - ( - - )} /> - ( - - )} /> + + + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> + ( + + )} /> ( - - )} /> - ( - - )} /> - - + + )} /> + ( + + )} /> + + ) } } function mapStateToProps (state) { return { - location: get(state, 'router.location.pathname') + location: get(state, 'router.location.pathname') } } diff --git a/ui/src/App/rootSagas.js b/ui/src/App/rootSagas.js index 6271bd80b..1a96b26c2 100644 --- a/ui/src/App/rootSagas.js +++ b/ui/src/App/rootSagas.js @@ -1,6 +1,7 @@ import { all } from 'redux-saga/effects'; import { testsRegister } from '../features/redux/saga/testsSagas'; import { processorsRegister } from '../features/redux/saga/processorsSagas'; +import { chaosExperimentsRegister } from '../features/redux/saga/chaosExperimentsSagas'; import { reportsRegister } from '../features/redux/saga/reportsSagas'; import { jobsRegister } from '../features/redux/saga/jobsSagas'; import { configRegister } from '../features/redux/saga/configSagas'; @@ -9,6 +10,7 @@ import { webhooksRegister } from '../features/redux/saga/webhooksSagas'; export default function * rootSaga () { yield all([ processorsRegister(), + chaosExperimentsRegister(), testsRegister(), reportsRegister(), jobsRegister(), diff --git a/ui/src/components/Dropdown/CustomDropdown.js b/ui/src/components/Dropdown/CustomDropdown.js new file mode 100644 index 000000000..621255a00 --- /dev/null +++ b/ui/src/components/Dropdown/CustomDropdown.js @@ -0,0 +1,23 @@ +import Dropdown from './Dropdown.export'; +import React from 'react'; + +const CustomDropdown = (props) => { + const { list, width, onChange, value, placeHolder, style } = props; + return ( + ({ key: option, value: option }))} + selectedOption={{ key: value, value: value }} + onChange={(selected) => { + onChange(selected.value) + }} + placeholder={placeHolder} + height={'35px'} + disabled={false} + validationErrorText='' + enableFilter={false} + /> + ) +} +export default CustomDropdown; diff --git a/ui/src/features/components/ChaosExperimentForm/index.js b/ui/src/features/components/ChaosExperimentForm/index.js new file mode 100644 index 000000000..dd2cab5d4 --- /dev/null +++ b/ui/src/features/components/ChaosExperimentForm/index.js @@ -0,0 +1,257 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import _ from 'lodash'; +import style from './style.scss'; +import * as Actions from '../../redux/action'; +import * as Selectors from '../../redux/selectors/chaosExperimentsSelector'; +import Modal from '../Modal'; +import Button from '../../../components/Button'; +import MonacoEditor from '@uiw/react-monacoeditor'; +import TitleInput from '../../../components/TitleInput'; +import Input from '../../../components/Input'; +import CustomDropdown from '../../../components/Dropdown/CustomDropdown'; + +const CHAOS_EXPERIMENT_KINDS = ['PodChaos', 'dnschaos', 'AWSChaos', 'HTTPChaos', 'StressChaos'] +const API_VERSION = 'chaos-mesh.org/v1alpha1' + +export class ChaosExperimentForm extends React.Component { + constructor (props) { + super(props); + this.state = { + name: '', + kind: '', + yaml: { + metadata: { + namespace: '', + name: '', + annotations: {} + }, + spec: {} + } + }; + } + + postChaosExperiment = () => { + const { createChaosExperiment } = this.props; + createChaosExperiment(createChaosExperimentRequest(this.state)); + }; + + componentDidUpdate (prevProps, prevState) { + const { createChaosExperimentSuccess: createChaosExperimentSuccessBefore } = prevProps; + const { + createChaosExperimentSuccess, + closeDialog + } = this.props; + + if (createChaosExperimentSuccess && !createChaosExperimentSuccessBefore) { + this.props.setCreateChaosExperimentSuccess(false); + closeDialog(); + } + } + + render () { + const { closeDialog } = this.props; + const { + name, + kind + } = this.state; + return ( + +

Create Chaos Experiment

+
+
+ {/* left */} +
+ + + +
+
+ + + +
+
+ + { + this.handleKindChange(value); + }} + placeHolder={'Kind'} + /> + +
+
+
+ {/* bottom */} + {this.generateJavascriptEditor()} + {this.generateBottomBar()} +
+ ); + } + + generateBottomBar = () => { + const { + isLoading, + closeDialog + } = this.props; + + return (
+
+ + +
+ +
); + }; + + onInputCodeChange = (code) => { + const isValidCode = testJSON(code); + if (!isValidCode) return; // Exit early if code is not valid JSON + + const parsedCode = JSON.parse(code); + this.setState((prevState) => { + const parsedName = _.get(parsedCode, 'metadata.name', prevState.name); + const parsedKind = _.get(parsedCode, 'kind', prevState.kind); + + if (_.isEqual(prevState.yaml, parsedCode)) return null; // Only update if the value is different + + return { + yaml: parsedCode, + name: parsedName, + kind: parsedKind + }; + }); + }; + + handleNameChange = (evt) => { + const newName = evt.target.value; + this.setState((prevState) => { + if (prevState.name === newName) return null; + return { + name: newName, + yaml: { + ...prevState.yaml, + metadata: { + ...prevState.yaml.metadata, + name: newName + } + } + }; + }); + }; + + handleKindChange = (value) => { + const newKind = value; + this.setState((prevState) => { + if (prevState.kind === newKind) return null; + return { + ...prevState, + kind: newKind + }; + }); + }; + + generateJavascriptEditor = () => { + const options = { + selectOnLineNumbers: true, + roundedSelection: false, + readOnly: false, + cursorStyle: 'line', + automaticLayout: false, + theme: 'vs' + }; + return ( +
+ {/* bottom */} +
+ { + this.onInputCodeChange(code); + }} + scrollbar={{ + // Subtle shadows to the left & top. Defaults to true. + useShadows: false, + // Render vertical arrows. Defaults to false. + verticalHasArrows: true, + // Render horizontal arrows. Defaults to false. + horizontalHasArrows: true, + // Render vertical scrollbar. + // Accepted values: 'auto', 'visible', 'hidden'. + // Defaults to 'auto' + vertical: 'visible', + // Render horizontal scrollbar. + // Accepted values: 'auto', 'visible', 'hidden'. + // Defaults to 'auto' + horizontal: 'visible', + verticalScrollbarSize: 17, + horizontalScrollbarSize: 17, + arrowSize: 30 + }} + /> + +
+
+ ); + }; +} + +function mapStateToProps (state) { + return { + isLoading: Selectors.chaosExperimentsLoading(state), + createChaosExperimentSuccess: Selectors.createChaosExperimentSuccess(state), + chaosExperimentsList: Selectors.chaosExperimentsList(state), + chaosExperimentsError: Selectors.chaosExperimentFailure(state) + }; +} + +function createChaosExperimentRequest (data) { + const { + name, + yaml + } = data; + return { + name, + kubeObject: { + kind: data.kind, + apiVersion: API_VERSION, + ...yaml + } + }; +} + +function testJSON (text) { + if (typeof text !== 'string') { + return false; + } + try { + JSON.parse(text); + return true; + } catch (error) { + return false; + } +} + +const mapDispatchToProps = { + createChaosExperiment: Actions.createChaosExperiment, + setCreateChaosExperimentSuccess: Actions.createChaosExperimentSuccess +}; +export default connect(mapStateToProps, mapDispatchToProps)(ChaosExperimentForm); diff --git a/ui/src/features/components/ChaosExperimentForm/style.scss b/ui/src/features/components/ChaosExperimentForm/style.scss new file mode 100644 index 000000000..4702ec740 --- /dev/null +++ b/ui/src/features/components/ChaosExperimentForm/style.scss @@ -0,0 +1,52 @@ + +.top { + border-bottom: 1px solid #e9e9e9; + padding: 10px 20px; + display: flex; + justify-content: space-between; + +} + +.bottom { + padding: 10px 20px 10px 0px; + display: flex; + justify-content: flex-start; + overflow: auto; + height: 50%; +} + +.top-inputs { + display: flex; + flex-direction: row; + flex-wrap:wrap; + flex:1; + justify-content: space-between; +} + +.input-container { + display: flex; + justify-content: space-between; + width: 350px; + align-items: center; + + &__title-input { + width: 100%; + margin-top: 2px; + } +} + +.plus-button-wrapper { + display: flex; + flex-direction: column; +} + +.buttons-container { + display: flex; + justify-content: flex-end; +} + +.form-button { + display: flex; + justify-content: space-between; + width: 230px; +} diff --git a/ui/src/features/components/TestForm/CustomDropdown.js b/ui/src/features/components/TestForm/CustomDropdown.js deleted file mode 100644 index f0e2281c1..000000000 --- a/ui/src/features/components/TestForm/CustomDropdown.js +++ /dev/null @@ -1,24 +0,0 @@ -import Dropdown from "../../../components/Dropdown/Dropdown.export"; -import React from "react"; - - -const CustomDropdown = (props) => { - const {list,width, onChange, value, placeHolder, style} = props; - return ( - ({key: option, value: option}))} - selectedOption={{key: value, value: value}} - onChange={(selected) => { - onChange(selected.value) - }} - placeholder={placeHolder} - height={'35px'} - disabled={false} - validationErrorText='' - enableFilter={false} - /> - ) -} -export default CustomDropdown; diff --git a/ui/src/features/components/TestForm/DynamicKeyValueInput.js b/ui/src/features/components/TestForm/DynamicKeyValueInput.js index 359cb8cb0..b7747473d 100644 --- a/ui/src/features/components/TestForm/DynamicKeyValueInput.js +++ b/ui/src/features/components/TestForm/DynamicKeyValueInput.js @@ -1,60 +1,58 @@ -import Input from "../../../components/Input"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faMinus, faPlus} from "@fortawesome/free-solid-svg-icons"; -import React from "react"; -import CustomDropdown from './CustomDropdown'; - -const DynamicKeyValueInput = ({value, onChange, onAdd, onDelete, keyHintText, valueHintText, dropdownOptions, dropDownOnChange, dropDownPlaceHolder}) => { - - const headersList = value - .map((keyValuePair, index) => { - return ( -
- {dropdownOptions && - dropDownOnChange(value, index)} - placeHolder={dropDownPlaceHolder} - /> - } - { - (!keyValuePair.onlyValue) && - { - onChange('key', evt.target.value, index) - }} placeholder={keyValuePair.keyPlaceholder || keyHintText || 'key'}/> - - } - - { - onChange('value', evt.target.value, index) - }} placeholder={keyValuePair.valuePlaceholder || valueHintText || 'value'}/> - - onDelete(index)} - icon={faMinus}/> -
- ) - }); - - return ( -
- {headersList} - onAdd()} - icon={faPlus}/> +import Input from '../../../components/Input'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; +import React from 'react'; +import CustomDropdown from '../../../components/Dropdown/CustomDropdown'; + +const DynamicKeyValueInput = ({ value, onChange, onAdd, onDelete, keyHintText, valueHintText, dropdownOptions, dropDownOnChange, dropDownPlaceHolder }) => { + const headersList = value + .map((keyValuePair, index) => { + return ( +
+ {dropdownOptions && + dropDownOnChange(value, index)} + placeHolder={dropDownPlaceHolder} + /> + } + { + (!keyValuePair.onlyValue) && + { + onChange('key', evt.target.value, index) + }} placeholder={keyValuePair.keyPlaceholder || keyHintText || 'key'} /> + + } + + { + onChange('value', evt.target.value, index) + }} placeholder={keyValuePair.valuePlaceholder || valueHintText || 'value'} /> + + onDelete(index)} + icon={faMinus} />
- ) + ) + }); + + return ( +
+ {headersList} + onAdd()} + icon={faPlus} /> +
+ ) } - export default DynamicKeyValueInput; diff --git a/ui/src/features/components/TestForm/StepForm.js b/ui/src/features/components/TestForm/StepForm.js index 02436cdd5..2b417e05c 100644 --- a/ui/src/features/components/TestForm/StepForm.js +++ b/ui/src/features/components/TestForm/StepForm.js @@ -18,7 +18,7 @@ import style from './stepform.scss'; import Input from '../../../components/Input'; import TitleInput from '../../../components/TitleInput'; import DynamicKeyValueInput from './DynamicKeyValueInput'; -import CustomDropdown from './CustomDropdown'; +import CustomDropdown from '../../../components/Dropdown/CustomDropdown'; import Expectations from './Expectations'; import ErrorWrapper from '../../../components/ErrorWrapper' import { URL_FIELDS } from '../../../validators/validate-urls'; diff --git a/ui/src/features/components/TestForm/constants.js b/ui/src/features/components/TestForm/constants.js index 30d8f2259..74a8f37f6 100644 --- a/ui/src/features/components/TestForm/constants.js +++ b/ui/src/features/components/TestForm/constants.js @@ -1,90 +1,92 @@ export const CONTENT_TYPES = { - APPLICATION_JSON: 'json', - FORM: 'x-www-form-urlencoded', - FORM_DATA: 'form-data', - OTHER: 'raw', - XML: 'xml', - NONE: 'none', + APPLICATION_JSON: 'json', + FORM: 'x-www-form-urlencoded', + FORM_DATA: 'form-data', + OTHER: 'raw', + XML: 'xml', + NONE: 'none' }; export const CAPTURE_TYPES = { - XPATH: 'XPath', - JSON_PATH: 'JSONPath', - REGEXP: 'Regexp', - HEADER: 'Header' + XPATH: 'XPath', + JSON_PATH: 'JSONPath', + REGEXP: 'Regexp', + HEADER: 'Header' }; export const CAPTURE_KEY_VALUE_PLACEHOLDER = { - XPath: { - key: '/id', - value: 'id' - }, - JSONPath: { - key: '$.id', - value: 'id' - }, - [CAPTURE_TYPES.REGEXP]: { - key: '/id', - value: 'id' - }, - [CAPTURE_TYPES.HEADER]: { - key: 'header-name', - value: 'id' - }, + XPath: { + key: '/id', + value: 'id' + }, + JSONPath: { + key: '$.id', + value: 'id' + }, + [CAPTURE_TYPES.REGEXP]: { + key: '/id', + value: 'id' + }, + [CAPTURE_TYPES.HEADER]: { + key: 'header-name', + value: 'id' + } }; export const CAPTURE_TYPE_TO_REQUEST = { - [CAPTURE_TYPES.XPATH]: 'xpath', - [CAPTURE_TYPES.JSON_PATH]: 'json', - [CAPTURE_TYPES.HEADER]: 'header', - [CAPTURE_TYPES.REGEXP]: 'regexp', + [CAPTURE_TYPES.XPATH]: 'xpath', + [CAPTURE_TYPES.JSON_PATH]: 'json', + [CAPTURE_TYPES.HEADER]: 'header', + [CAPTURE_TYPES.REGEXP]: 'regexp' }; export const CAPTURE_RES_TYPE_TO_CAPTURE_TYPE = { - json: CAPTURE_TYPES.JSON_PATH, - xpath: CAPTURE_TYPES.XPATH, - header: CAPTURE_TYPES.HEADER, - regexp: CAPTURE_TYPES.REGEXP + json: CAPTURE_TYPES.JSON_PATH, + xpath: CAPTURE_TYPES.XPATH, + header: CAPTURE_TYPES.HEADER, + regexp: CAPTURE_TYPES.REGEXP }; export const SUPPORTED_CONTENT_TYPES = [CONTENT_TYPES.NONE, CONTENT_TYPES.FORM_DATA, CONTENT_TYPES.FORM, CONTENT_TYPES.APPLICATION_JSON, CONTENT_TYPES.OTHER]; export const SUPPORTED_CAPTURE_TYPES = [CAPTURE_TYPES.JSON_PATH, CAPTURE_TYPES.XPATH, CAPTURE_TYPES.REGEXP, CAPTURE_TYPES.HEADER]; export const HTTP_METHODS = ['POST', 'GET', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE']; +export const CHAOS_EXPERIMENT_KINDS = ['PodChaos', 'dnschaos', 'AWSChaos', 'HTTPChaos', 'StressChaos'] + export const EXPECTATIONS_TYPE = { - STATUS_CODE: 'statusCode' + STATUS_CODE: 'statusCode' }; export const EXPECTATIONS_SPEC = [ - { - propertyName: EXPECTATIONS_TYPE.STATUS_CODE, - onlyValue: true - }, - { - propertyName: 'contentType', - onlyValue: true - }, - { - propertyName: 'hasProperty', - onlyValue: true - }, + { + propertyName: EXPECTATIONS_TYPE.STATUS_CODE, + onlyValue: true + }, + { + propertyName: 'contentType', + onlyValue: true + }, + { + propertyName: 'hasProperty', + onlyValue: true + }, - { - propertyName: 'hasHeader', - onlyValue: true - }, - { - propertyName: 'headerEquals', - onlyValue: false - }, - { - propertyName: 'equals', - onlyValue: false - }, - { - propertyName: 'matchesRegexp', - onlyValue: true - }, + { + propertyName: 'hasHeader', + onlyValue: true + }, + { + propertyName: 'headerEquals', + onlyValue: false + }, + { + propertyName: 'equals', + onlyValue: false + }, + { + propertyName: 'matchesRegexp', + onlyValue: true + } ]; export const EXPECTATIONS_SPEC_BY_PROP = EXPECTATIONS_SPEC - .reduce((acc, cur) => { - acc[cur.propertyName] = cur; - return acc; - }, {}); + .reduce((acc, cur) => { + acc[cur.propertyName] = cur; + return acc; + }, {}); diff --git a/ui/src/features/configurationColumn.js b/ui/src/features/configurationColumn.js index 12a5141d2..12cdf30e0 100644 --- a/ui/src/features/configurationColumn.js +++ b/ui/src/features/configurationColumn.js @@ -1,30 +1,30 @@ -import {TableHeader} from "../components/ReactTable"; -import React, {useEffect, useState} from "react"; -import {get} from 'lodash'; +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'; import 'font-awesome/css/font-awesome.min.css'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { - faEye, - faRedo, - faRunning, - faCloudDownloadAlt, - faStopCircle, - faTrashAlt, - faClone, - faPen + faEye, + faRedo, + faRunning, + faCloudDownloadAlt, + faStopCircle, + faTrashAlt, + faClone, + faPen } from '@fortawesome/free-solid-svg-icons' import classnames from 'classnames'; import css from './configurationColumn.scss'; -import env from "../App/common/env"; -import {v4 as uuid} from "uuid"; +import env from '../App/common/env'; +import { v4 as uuid } from 'uuid'; import TooltipWrapper from '../components/TooltipWrapper'; -import {getTimeFromCronExpr} from './utils'; +import { getTimeFromCronExpr } from './utils'; import UiSwitcher from '../components/UiSwitcher'; -import TextArea from "../components/TextArea"; +import TextArea from '../components/TextArea'; import ClickOutHandler from 'react-onclickout' const iconsWidth = 50; @@ -33,579 +33,604 @@ 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, onClone}) => { - - const columns = [ - { - id: 'compare', - Header: () => ( - - Select - - ), - accessor: (data) => , - width: iconsWidth - }, { - id: 'report_id', - Header: () => ( - - Test Name - - ), - accessor: 'report_id', - }, - { - id: 'name', - Header: () => ( - - Test Name - - ), - accessor: 'name', - }, { - id: 'processor_name', - Header: () => ( - - Processor Name - - ), - accessor: 'name', - }, - { - id: 'description', - Header: () => ( - - Description - - ), - accessor: 'description' - }, { - id: 'updated_at', - Header: () => ( - -1 && sortHeader.indexOf('+') > -1} - down={sortHeader.indexOf('updated_at') > -1 && sortHeader.indexOf('-') > -1} - onClick={() => { - onSort('updated_at') - }} - > - Modified - - ), - accessor: (data) => (dateFormatter(data.updated_at)), - width: extraExLargeSize + 20, - className: css['center-flex'], - }, { - id: 'type', - Header: () => ( - - Type - - ), - accessor: 'type', - width: iconsWidth, - className: css['center-flex'], - }, { - id: 'edit', - Header: () => ( - - Edit - - ), - accessor: data => data.type === 'basic' ? { - e.stopPropagation(); - onEdit(data) - }}/> : - - DSL not supported -
} - dataId={ - `tooltipKey`} - place='top' - offset={{top: 1}} - > -
- N/A -
- , - width: iconsWidth, - className: css['center-flex'], - }, - { - id: 'processor_edit', - Header: () => ( - - Edit - - ), - accessor: data => { - e.stopPropagation(); - onEdit(data) - }}/>, - width: iconsWidth, - className: css['center-flex'], - }, - { - id: 'job_edit', - Header: () => ( - - Edit - - ), - accessor: data => { - e.stopPropagation(); - onEdit(data) - }}/>, - width: iconsWidth, - className: css['center-flex'], - }, - { - id: 'test_name', - Header: () => ( - - Test Name - - ), - accessor: 'test_name', - - - }, - { - id: 'start_time', - Header: () => ( - -1 && sortHeader.indexOf('+') > -1} - down={sortHeader.indexOf('start_time') > -1 && sortHeader.indexOf('-') > -1} - onClick={() => { - onSort('start_time') - }} - > - Start Time - - ), - accessor: data => (
{dateFormatter(data.start_time)}
), - }, - { - id: 'end_time', - Header: () => ( - - End Time - - ), - accessor: data => (
{dateFormatter(data.end_time)}
), - width: extraExLargeSize, - className: css['center-flex'], - }, - { - id: 'duration', - Header: () => ( - - Duration - - ), - accessor: data => (prettySeconds(data.duration)), - width: largeSize, - // className: css['center-flex'], - }, - { - id: 'status', - Header: () => ( - - Status - - ), - accessor: data => statusFormatter(data.status), - width: largeSize - }, - { - id: 'arrival_rate', - Header: () => ( - - Rate - - ), - accessor: data => data.arrival_rate || data.arrival_count, - width: mediumSize, - className: css['center-flex'], - }, - { - id: 'ramp_to', - Header: () => ( - - Ramp To - - ), - accessor: data => (data.ramp_to || 'N/A'), - width: mediumSize, - className: css['center-flex'], - }, - { - id: 'max_virtual_users', - Header: () => ( - - Max Virtual Users - - ), - accessor: data => (data.max_virtual_users || 'N/A'), - width: extraExLargeSize, - className: css['center-flex'], - }, - { - id: 'cron_expression', - Header: () => ( - - Cron Expression - - ), - accessor: data => (getTimeFromCronExpr(data.cron_expression) || 'N/A'), - width: extraExLargeSize, - className: css['center-flex'], - }, - { - id: 'last_run', - Header: () => ( - - Last Run - - ), - accessor: 'last_run', - // minWidth: 150 - }, +export const getColumns = ({ columnsNames, sortHeader = '', onSort, onReportView, onRawView, onStop, onDelete, onEdit, onRunTest, onEnableDisable, onEditNote, selectedReports, onReportSelected, onClone }) => { + const columns = [ + { + id: 'compare', + Header: () => ( + + Select + + ), + accessor: (data) => , + width: iconsWidth + }, { + id: 'report_id', + Header: () => ( + + Test Name + + ), + accessor: 'report_id' + }, + { + id: 'name', + Header: () => ( + + Test Name + + ), + accessor: 'name' + }, { + id: 'processor_name', + Header: () => ( + + Processor Name + + ), + accessor: 'name' + }, + { + id: 'experiment_name', + Header: () => ( + + Experiment Name + + ), + accessor: 'name' + }, + { + id: 'description', + Header: () => ( + + Description + + ), + accessor: 'description' + }, { + id: 'kind', + Header: () => ( + + Kind + + ), + accessor: 'kind' + }, + { + id: 'updated_at', + Header: () => ( + -1 && sortHeader.indexOf('+') > -1} + down={sortHeader.indexOf('updated_at') > -1 && sortHeader.indexOf('-') > -1} + onClick={() => { + onSort('updated_at') + }} + > + Modified + + ), + accessor: (data) => (dateFormatter(data.updated_at)), + width: extraExLargeSize + 20, + className: css['center-flex'] + }, + { + id: 'created_at', + Header: () => ( + -1 && sortHeader.indexOf('+') > -1} + down={sortHeader.indexOf('created_at') > -1 && sortHeader.indexOf('-') > -1} + onClick={() => { + onSort('created_at') + }} + > + Created + + ), + accessor: (data) => (dateFormatter(data.updated_at)), + width: extraExLargeSize + 20, + className: css['center-flex'] + }, { + id: 'type', + Header: () => ( + + Type + + ), + accessor: 'type', + width: iconsWidth, + className: css['center-flex'] + }, { + id: 'edit', + Header: () => ( + + Edit + + ), + accessor: data => data.type === 'basic' ? { + e.stopPropagation(); + onEdit(data) + }} /> + : + DSL not supported + } + dataId={ + 'tooltipKey'} + place='top' + offset={{ top: 1 }} + > +
+ N/A +
+
, + width: iconsWidth, + className: css['center-flex'] + }, + { + id: 'processor_edit', + Header: () => ( + + Edit + + ), + accessor: data => { + e.stopPropagation(); + onEdit(data) + }} />, + width: iconsWidth, + className: css['center-flex'] + }, + { + id: 'job_edit', + Header: () => ( + + Edit + + ), + accessor: data => { + e.stopPropagation(); + onEdit(data) + }} />, + width: iconsWidth, + className: css['center-flex'] + }, + { + id: 'test_name', + Header: () => ( + + Test Name + + ), + accessor: 'test_name' - { - id: 'last_success_rate', - Header: () => ( - - Success Rate - - ), - accessor: data => (Math.floor(data.last_success_rate) + '%'), - width: extraLargeSize, - className: css['center-flex'], - }, - { - id: 'avg_rps', - Header: () => ( - - RPS - - ), - accessor: data => (Math.floor(data.avg_rps === undefined ? data.last_rps : data.avg_rps)), - width: iconsWidth, - className: css['center-flex'], - }, + }, + { + id: 'start_time', + Header: () => ( + -1 && sortHeader.indexOf('+') > -1} + down={sortHeader.indexOf('start_time') > -1 && sortHeader.indexOf('-') > -1} + onClick={() => { + onSort('start_time') + }} + > + Start Time + + ), + accessor: data => (
{dateFormatter(data.start_time)}
) + }, + { + id: 'end_time', + Header: () => ( + + End Time + + ), + accessor: data => (
{dateFormatter(data.end_time)}
), + width: extraExLargeSize, + className: css['center-flex'] + }, + { + id: 'duration', + Header: () => ( + + Duration + + ), + accessor: data => (prettySeconds(data.duration)), + width: largeSize + // className: css['center-flex'], + }, + { + id: 'status', + Header: () => ( + + Status + + ), + accessor: data => statusFormatter(data.status), + width: largeSize + }, + { + id: 'arrival_rate', + Header: () => ( + + Rate + + ), + accessor: data => data.arrival_rate || data.arrival_count, + width: mediumSize, + className: css['center-flex'] + }, + { + id: 'ramp_to', + Header: () => ( + + Ramp To + + ), + accessor: data => (data.ramp_to || 'N/A'), + width: mediumSize, + className: css['center-flex'] + }, + { + id: 'max_virtual_users', + Header: () => ( + + Max Virtual Users + + ), + accessor: data => (data.max_virtual_users || 'N/A'), + width: extraExLargeSize, + className: css['center-flex'] + }, + { + id: 'cron_expression', + Header: () => ( + + Cron Expression + + ), + accessor: data => (getTimeFromCronExpr(data.cron_expression) || 'N/A'), + width: extraExLargeSize, + className: css['center-flex'] + }, + { + id: 'last_run', + Header: () => ( + + Last Run + + ), + accessor: 'last_run' + // minWidth: 150 + }, - { - id: 'parallelism', - Header: () => ( - - Parallelism - - ), - accessor: 'parallelism', - width: largeSize, - className: css['center-flex'], - }, - { - id: 'notes', - Header: () => ( - - Notes - - ), - accessor: data => , - }, - { - id: 'score', - Header: () => ( - - Score - - ), - accessor: (data) => { - if (data.score) { - const color = get(data, 'benchmark_weights_data.benchmark_threshold', 0) <= data.score ? 'green' : 'red'; - return ( - {Math.floor(data.score)} - ) - } - }, - width: iconsWidth - }, { - id: 'report', - Header: () => ( - - Report - - ), - accessor: data => { - e.stopPropagation(); - onReportView(data) - }}/>, - width: mediumSize - }, - { - id: 'grafana_report', - Header: () => ( - - Grafana - - ), - accessor: data => { - e.stopPropagation(); - window.open(data.grafana_report, '_blank') - }}/>, - width: mediumSize - }, - { - id: 'raw', - Header: () => ( - - Raw - - ), - accessor: data => { - e.stopPropagation(); - onRawView(data) - }}/>, - width: iconsWidth, - className: css['center-flex'], - }, - { - id: 'rerun', - Header: () => ( - - Rerun - - ), - accessor: data => { - e.stopPropagation(); - onRunTest(data) - }}/>, - width: iconsWidth - }, - { - id: 'run_now', - Header: () => ( - - Run Now - - ), - accessor: data => { - e.stopPropagation(); - onRunTest(data) - }}/>, - width: semiLarge, - className: css['center-flex'], - }, - { - id: 'delete', - Header: () => ( - - Delete - - ), - accessor: data => { - e.stopPropagation(); - onDelete(data) - }}/>, - width: mediumSize, - className: css['center-flex'], - }, { - id: 'clone', - Header: () => ( - - Clone - - ), - accessor: data => { - e.stopPropagation(); - onClone(data) - }}/>, - width: mediumSize, - className: css['center-flex'], - }, - { - id: 'run_test', - Header: () => ( - - Run Test - - ), - accessor: data => { - e.stopPropagation(); - onRunTest(data) - }}/>, - width: semiLarge, - className: css['center-flex'], - }, { - id: 'logs', - Header: () => ( - - Logs - - ), - accessor: data => ( { - e.stopPropagation(); - window.open(`${env.PREDATOR_URL}/jobs/${data.job_id}/runs/${data.report_id}/logs`, '_blank') - }}/>), - width: iconsWidth + { + id: 'last_success_rate', + Header: () => ( + + Success Rate + + ), + accessor: data => (Math.floor(data.last_success_rate) + '%'), + width: extraLargeSize, + className: css['center-flex'] + }, + { + id: 'avg_rps', + Header: () => ( + + RPS + + ), + accessor: data => (Math.floor(data.avg_rps === undefined ? data.last_rps : data.avg_rps)), + width: iconsWidth, + className: css['center-flex'] + }, - }, { - id: 'stop', - Header: () => ( - - Stop - - ), - accessor: (data) => { - const disabled = (data.status !== 'in_progress' && data.status !== 'started'); - return ( { - e.stopPropagation(); - onStop(data) - }}/>) - }, - width: iconsWidth - }, - { - id: 'enabled_disabled', - Header: () => ( - - Enabled - - ), - accessor: (data) => { - const activated = (typeof data.enabled === 'undefined' ? true : data.enabled); - return ( -
- { - onEnableDisable(data, value) - }} - disabledInp={false} - activeState={activated} - height={12} - width={22} - /> -
) - }, - width: semiLarge, - className: css['center-flex'], + { + id: 'parallelism', + Header: () => ( + + Parallelism + + ), + accessor: 'parallelism', + width: largeSize, + className: css['center-flex'] + }, + { + id: 'notes', + Header: () => ( + + Notes + + ), + accessor: data => + }, + { + id: 'score', + Header: () => ( + + Score + + ), + accessor: (data) => { + if (data.score) { + const color = get(data, 'benchmark_weights_data.benchmark_threshold', 0) <= data.score ? 'green' : 'red'; + return ( + {Math.floor(data.score)} + ) } - ]; + }, + width: iconsWidth + }, { + id: 'report', + Header: () => ( + + Report + + ), + accessor: data => { + e.stopPropagation(); + onReportView(data) + }} />, + width: mediumSize + }, + { + id: 'grafana_report', + Header: () => ( + + Grafana + + ), + accessor: data => { + e.stopPropagation(); + window.open(data.grafana_report, '_blank') + }} />, + width: mediumSize + }, + { + id: 'raw', + Header: () => ( + + Raw + + ), + accessor: data => { + e.stopPropagation(); + onRawView(data) + }} />, + width: iconsWidth, + className: css['center-flex'] + }, + { + id: 'rerun', + Header: () => ( + + Rerun + + ), + accessor: data => { + e.stopPropagation(); + onRunTest(data) + }} />, + width: iconsWidth + }, + { + id: 'run_now', + Header: () => ( + + Run Now + + ), + accessor: data => { + e.stopPropagation(); + onRunTest(data) + }} />, + width: semiLarge, + className: css['center-flex'] + }, + { + id: 'delete', + Header: () => ( + + Delete + + ), + accessor: data => { + e.stopPropagation(); + onDelete(data) + }} />, + width: mediumSize, + className: css['center-flex'] + }, { + id: 'clone', + Header: () => ( + + Clone + + ), + accessor: data => { + e.stopPropagation(); + onClone(data) + }} />, + width: mediumSize, + className: css['center-flex'] + }, + { + id: 'run_test', + Header: () => ( + + Run Test + + ), + accessor: data => { + e.stopPropagation(); + onRunTest(data) + }} />, + width: semiLarge, + className: css['center-flex'] + }, { + id: 'logs', + Header: () => ( + + Logs + + ), + accessor: data => ( { + e.stopPropagation(); + window.open(`${env.PREDATOR_URL}/jobs/${data.job_id}/runs/${data.report_id}/logs`, '_blank') + }} />), + width: iconsWidth + }, { + id: 'stop', + Header: () => ( + + Stop + + ), + accessor: (data) => { + const disabled = (data.status !== 'in_progress' && data.status !== 'started'); + return ( { + e.stopPropagation(); + onStop(data) + }} />) + }, + width: iconsWidth + }, + { + id: 'enabled_disabled', + Header: () => ( + + Enabled + + ), + accessor: (data) => { + const activated = (typeof data.enabled === 'undefined' ? true : data.enabled); + return ( +
+ { + onEnableDisable(data, value) + }} + disabledInp={false} + activeState={activated} + height={12} + width={22} + /> +
) + }, + width: semiLarge, + className: css['center-flex'] + } + ]; - return columnsNames.map((name) => { - const column = columns.find((c) => c.id === name); - if (!column) { - throw new Error(`column ${name} not found`); - } - return column; - }); + return columnsNames.map((name) => { + const column = columns.find((c) => c.id === name); + if (!column) { + throw new Error(`column ${name} not found`); + } + return column; + }); }; - const dateFormatter = (cell, row) => { - const timePattern = 'DD-MM-YYYY hh:mm:ss a'; + const timePattern = 'DD-MM-YYYY hh:mm:ss a'; - if (!cell) { - return 'Still running...'; - } else { - return ( - new Moment(cell).local().format('lll') - ); - } + if (!cell) { + return 'Still running...'; + } else { + return ( + new Moment(cell).local().format('lll') + ); + } }; -const ViewButton = ({onClick, icon, disabled, text}) => { - - const element = icon ? !disabled && onClick} icon={icon}/> : text || 'View'; +const ViewButton = ({ onClick, icon, disabled, text }) => { + const element = icon ? !disabled && onClick} icon={icon} /> : text || 'View'; - - return (
{element}
) + return (
{element}
) }; -const CompareCheckbox = ({data, onReportSelected, selectedReports}) => { - - return ( -
- onReportSelected(data.test_id, data.report_id, value)} - /> -
- ) +const CompareCheckbox = ({ data, onReportSelected, selectedReports }) => { + return ( +
+ onReportSelected(data.test_id, data.report_id, value)} + /> +
+ ) } -const Notes = ({data, onEditNote}) => { - const {report_id, test_id} = data; - const notes = data.notes || ''; - const [editMode, setEditMode] = useState(false); - const [editValue, setEditValue] = useState(notes); - const id = uuid(); - const cell = notes.split('\n').map((row,index) => (

{row}

)); +const Notes = ({ data, onEditNote }) => { + const { report_id, test_id } = data; + const notes = data.notes || ''; + const [editMode, setEditMode] = useState(false); + const [editValue, setEditValue] = useState(notes); + const id = uuid(); + const cell = notes.split('\n').map((row, index) => (

{row}

)); - function onKeyDown(e) { - if (e.key === 'Enter') { - save(); - } + function onKeyDown (e) { + if (e.key === 'Enter') { + save(); } + } - function save() { - if (editMode) { - setEditMode(false); - onEditNote(test_id, report_id, editValue); - } + function save () { + if (editMode) { + setEditMode(false); + onEditNote(test_id, report_id, editValue); } + } - return( + return ( - - {cell} - } - dataId={`tooltipKey_${id}`} - place='top' - offset={{top: 1}} - > -
- {editMode && - -