diff --git a/package-lock.json b/package-lock.json index 7a98ffeac..148e8b467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1671,7 +1671,7 @@ "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "anymatch": { "version": "3.1.1", @@ -2829,7 +2829,7 @@ "cls-bluebird": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", - "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", + "integrity": "sha512-XVb0RPmHQyy35Tz9z34gvtUcBKUK8A/1xkGCyeFc9B0C7Zr5SysgFaswRVdwI5NEMcO+3JKlIDGIOgERSn9NdA==", "requires": { "is-bluebird": "^1.0.2", "shimmer": "^1.1.0" @@ -4726,9 +4726,9 @@ } }, "dottie": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", - "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, "driftless": { "version": "2.0.3", @@ -7115,7 +7115,7 @@ "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + "integrity": "sha512-lRy4DxuIFWXlJU7ed8UiTJOSTqStqYdEb4CEbtXfNbkdj3nH1L+reUWiE10VWcJS2yR7tge8Z74pJjtBjNwj0w==" }, "inflight": { "version": "1.0.6", @@ -7320,7 +7320,7 @@ "is-bluebird": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", - "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" + "integrity": "sha512-PDRu1vVip5dGQg5tfn2qVCCyxbBYu5MhYUJwSfL/RoGBI97n1fxvilVazxzptZW0gcmsMH17H4EVZZI5E/RSeA==" }, "is-buffer": { "version": "1.1.6", @@ -10959,9 +10959,9 @@ "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" }, "sequelize": { - "version": "5.22.3", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.22.3.tgz", - "integrity": "sha512-+nxf4TzdrB+PRmoWhR05TP9ukLAurK7qtKcIFv5Vhxm5Z9v+d2PcTT6Ea3YAoIQVkZ47QlT9XWAIUevMT/3l8Q==", + "version": "5.22.5", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.22.5.tgz", + "integrity": "sha512-ySIHof18sJbeVG4zjEvsDL490cd9S14/IhkCrZR/g0C/FPlZq1AzEJVeSAo++9/sgJH2eERltAIGqYQNgVqX/A==", "requires": { "bluebird": "^3.5.0", "cls-bluebird": "^2.1.0", @@ -10975,23 +10975,33 @@ "semver": "^6.3.0", "sequelize-pool": "^2.3.0", "toposort-class": "^1.0.1", - "uuid": "^3.3.3", - "validator": "^10.11.0", + "uuid": "^8.3.2", + "validator": "^13.7.0", "wkx": "^0.4.8" }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" } } }, @@ -12292,7 +12302,7 @@ "toposort-class": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", - "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" }, "tough-cookie": { "version": "2.5.0", diff --git a/package.json b/package.json index 2ea372583..9d13049e9 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "request": "^2.88.2", "request-promise-native": "^1.0.8", "semver": "^7.3.2", - "sequelize": "^5.22.3", + "sequelize": "^5.22.5", "slack-emojis": "^1.1.1", "sqlite3": "^5.1.6", "tedious": "^9.2.2", diff --git a/tests/configurations/dockerRun.sh b/tests/configurations/dockerRun.sh index 4bef518a3..0500d474a 100755 --- a/tests/configurations/dockerRun.sh +++ b/tests/configurations/dockerRun.sh @@ -27,7 +27,7 @@ function mysql() { IMAGE_NAME=mysql:5.7 APP=mysql stop $APP - COMMAND="docker run \ + COMMAND="docker run --platform linux/amd64\ -d \ --name mysql \ -p 3306:3306 \ 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/components/ErrorWrapper/index.js b/ui/src/components/ErrorWrapper/index.js index 33035644a..0b278bb5d 100644 --- a/ui/src/components/ErrorWrapper/index.js +++ b/ui/src/components/ErrorWrapper/index.js @@ -7,7 +7,7 @@ const ErrorWrapper = (props) => { const hasError = !!errorText return ( - {React.cloneElement(children, { error: hasError })} + {React.cloneElement(children)} {hasError && (
{errorText} diff --git a/ui/src/features/components/ChaosExperimentForm/index.js b/ui/src/features/components/ChaosExperimentForm/index.js new file mode 100644 index 000000000..7df25b31e --- /dev/null +++ b/ui/src/features/components/ChaosExperimentForm/index.js @@ -0,0 +1,283 @@ +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'; +import ErrorWrapper from '../../../components/ErrorWrapper'; +import validateKubeObject from './validator'; + +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); + if (props.chaosExperimentForEdit) { + this.state = { + validationErrorText: '', + name: props.chaosExperimentForEdit.kubeObject.metadata.name, + kind: props.chaosExperimentForEdit.kubeObject.kind, + yaml: props.chaosExperimentForEdit.kubeObject + } + } else { + this.state = { + validationErrorText: '', + name: '', + kind: '', + yaml: { + metadata: { + namespace: '', + name: '', + annotations: {} + }, + spec: { + duration: '0ms' + } + } + }; + } + } + handleExperimentSubmit = () => { + const { createChaosExperiment } = this.props; + const chaosExperimentRequest = createChaosExperimentRequest(this.state) + const validationError = validateKubeObject(chaosExperimentRequest.kubeObject) + if (validationError) { + this.setState({ validationErrorText: validationError }) + } else { + createChaosExperiment(chaosExperimentRequest); + } + } + + componentDidUpdate (prevProps, prevState) { + const { createChaosExperimentSuccess: createChaosExperimentSuccessBefore } = prevProps; + const { + createChaosExperimentSuccess, + closeDialog + } = this.props; + + if (createChaosExperimentSuccess && !createChaosExperimentSuccessBefore) { + this.props.setCreateChaosExperimentSuccess(false); + closeDialog(); + } + } + + render () { + const { closeDialog, chaosExperimentForEdit } = this.props; + const { + name, + kind + } = this.state; + return ( + +

Create Chaos Experiment

+
+ {!chaosExperimentForEdit && ( +
+ {/* left */} +
+ + + +
+
+ + + +
+
+ + { + this.handleKindChange(value); + }} + placeHolder={'Kind'} + /> + +
+
+ )} +
+ {/* bottom */} + {this.generateJavascriptEditor()} + {this.generateBottomBar()} +
+ ); + } + + generateBottomBar = () => { + const { + isLoading, + closeDialog + } = this.props; + + return ( +
+
+ + +
+
+ ); + }; + + onInputCodeChange = (code) => { + this.setState({ validationErrorText: '' }) + 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: name || yaml.metadata.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/ChaosExperimentForm/validator.js b/ui/src/features/components/ChaosExperimentForm/validator.js new file mode 100644 index 000000000..228a9e0a7 --- /dev/null +++ b/ui/src/features/components/ChaosExperimentForm/validator.js @@ -0,0 +1,83 @@ + +const SCHEMA = { + type: 'object', + properties: { + apiVersion: { + type: 'string', + pattern: new RegExp('^chaos-mesh\\.org\\/v1alpha1$') + }, + kind: { + type: 'string', + options: ['PodChaos', 'DNSChaos', 'AWSChaos', 'HTTPChaos', 'StressChaos'] + }, + metadata: { + type: 'object', + properties: { + name: { type: 'string', required: true }, + namespace: { type: 'string', required: true } + }, + required: ['name', 'namespace'] + }, + spec: { + type: 'object', + properties: { + duration: { + type: 'string', + pattern: new RegExp('^[0-9]+(ms|s|m|h)$') + }, + required: ['duration'] + } + } + }, + required: ['apiVersion', 'kind', 'metadata', 'spec'] +}; + +const validateKubeObject = (jsonObject) => { + const validationError = validateProperty(jsonObject, SCHEMA); + + if (validationError) { + return validationError; + } +} + +function validateProperty (object, schema) { + for (const key in schema.properties) { + const propertySchema = schema.properties[key]; + + if (propertySchema.required && !object.hasOwnProperty(key)) { + return `Required property "${key}" is missing.`; + } + + if (object.hasOwnProperty(key)) { + const propertyValue = object[key]; + const { type, pattern, required, options } = propertySchema; + + if (type === 'object' && typeof propertyValue !== 'object') { + return `Property "${key}" must be an object.`; + } else if (type === 'string' && typeof propertyValue !== 'string') { + return `Property "${key}" must be a string.`; + } else if (type === 'string' && typeof propertyValue === 'string' && required && !propertyValue.trim().length) { + return `Property "${key}" must be a non empty string.`; + } else if (type === 'string' && pattern) { + if (!pattern.test(propertyValue)) { + return `Property "${key}" value must be in supported pattern "${pattern}".`; + } + } else if (type === 'string' && options) { + if (!options.includes(propertyValue)) { + return `Property "${key}" value must be in supported options "[${options.join(', ')}]".`; + } + } + + if (propertySchema.properties) { + const nestedValidationResult = validateProperty(propertyValue, propertySchema); + if (nestedValidationResult !== null) { + return `Invalid value for property "${key}": ${nestedValidationResult}`; + } + } + } + } + + return null; +} + +export default validateKubeObject; 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..112d3e611 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,626 @@ 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: 'duration', + Header: () => ( + + Duration + + ), + accessor: 'duration' + }, + { + 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: 'experiment_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 && - -