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
-
- Schema
-
+
+
+ Schema
+
+