diff --git a/ui/job-view/CustomJobActions.jsx b/ui/job-view/CustomJobActions.jsx index 8b0e51fc68f..70b133c624a 100644 --- a/ui/job-view/CustomJobActions.jsx +++ b/ui/job-view/CustomJobActions.jsx @@ -5,7 +5,7 @@ import jsonSchemaDefaults from 'json-schema-defaults'; import keyBy from 'lodash/keyBy'; import jsyaml from 'js-yaml'; import tcLibUrls from 'taskcluster-lib-urls'; -import { Button, Dropdown, Form, Modal } from 'react-bootstrap'; +import { Button, Dropdown, Form, Modal, ButtonGroup } from 'react-bootstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheckSquare } from '@fortawesome/free-regular-svg-icons'; @@ -16,6 +16,7 @@ import { checkRootUrl } from '../taskcluster-auth-callback/constants'; import { notify } from '../shared/stores/notificationStore'; import { usePushesStore } from '../shared/stores/pushesStore'; +import SimpleTooltip from '../shared/SimpleTooltip'; class CustomJobActions extends React.PureComponent { constructor(props) { @@ -32,6 +33,7 @@ class CustomJobActions extends React.PureComponent { schema: '', payload: '', dropdownOpen: false, + payloadView: 'yaml', }; } @@ -79,7 +81,7 @@ class CustomJobActions extends React.PureComponent { const selectedAction = actions[actionName]; if (actionName) { - this.setState({ selectedAction }); + this.setState({ selectedAction, payloadView: 'yaml' }); this.updateSelectedAction(selectedAction); } }; @@ -186,6 +188,132 @@ class CustomJobActions extends React.PureComponent { } }; + onFormFieldChange = (fieldName, type, value) => { + const { payload } = this.state; + try { + const parsedPayload = jsyaml.load(payload) || {}; + + if (type === 'integer') { + parsedPayload[fieldName] = value === '' ? '' : parseInt(value, 10); + } else if (type === 'boolean') { + parsedPayload[fieldName] = value === 'true'; + } + + this.onChangePayload(jsyaml.dump(parsedPayload)); + } catch (_e) { + notify(`Cannot update form: Invalid YAML in Raw view.`, 'danger'); + } + }; + + renderPayloadForm() { + const { selectedAction, payload } = this.state; + const properties = selectedAction?.schema?.properties; + + if (!properties) return null; + + let parsedPayload = {}; + try { + parsedPayload = jsyaml.load(payload) || {}; + } catch (_e) { + return ( +
+ Invalid YAML detected. Please fix errors in the Raw YAML view before using the form. +
+ ); + } + + return ( +
+ {Object.entries(properties).map(([key, prop]) => { + const isBool = prop.type === 'boolean'; + const isInt = prop.type === 'integer'; + + const isEmpty = parsedPayload[key] === '' || parsedPayload[key] === undefined || parsedPayload[key] === null || Number.isNaN(parsedPayload[key]); + const value = !isEmpty ? parsedPayload[key] : (parsedPayload[key] === '' ? '' : prop.default); + + const label = prop.title || key; + const rangeLabel = isInt && prop.minimum !== undefined && prop.maximum !== undefined + ? ` (${prop.minimum}-${prop.maximum})` + : ''; + const fullLabelText = `${label}${rangeLabel}`; + + return ( + + + {prop.description ? ( + + ) : ( + fullLabelText + )} + +
+ {isBool ? ( + this.onFormFieldChange(key, 'boolean', e.target.value)} + > + + + + ) : isInt ? ( + + { + let sanitizedValue = e.target.value.replace(/\D/g, ''); + + if (sanitizedValue !== '' && prop.maximum !== undefined && parseInt(sanitizedValue, 10) > prop.maximum) { + sanitizedValue = prop.maximum.toString(); + } + + this.onFormFieldChange(key, 'integer', sanitizedValue); + }} + onBlur={(e) => { + const fallback = prop.default !== undefined + ? prop.default.toString() + : (prop.minimum !== undefined ? prop.minimum.toString() : '0'); + + if (e.target.value === '') { + this.onFormFieldChange(key, 'integer', fallback); + } else { + const numVal = parseInt(e.target.value, 10); + if (prop.minimum !== undefined && numVal < prop.minimum) { + this.onFormFieldChange(key, 'integer', prop.minimum.toString()); + } + } + }} + onKeyDown={(e) => { + const isControlKey = ['Backspace', 'Tab', 'Enter', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key); + const isShortcut = e.ctrlKey || e.metaKey; + const isNumber = /^[0-9]$/.test(e.key); + + if (!isControlKey && !isShortcut && !isNumber) { + e.preventDefault(); + } + }} + /> + + This field cannot be empty. + + + ) : null} +
+
+ ); + })} +
+ ); + } + render() { const { toggle } = this.props; const { triggering, selectedAction, schema, actions, payload } = this.state; @@ -205,7 +333,7 @@ class CustomJobActions extends React.PureComponent { {!!actions && (
- Action + Action
- - Payload - -