diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17e1829cc7..4da80092a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ This is a list of high-level changes for each release of AWX. A full list of com
- Added pending workflow approval count to the application header https://github.com/ansible/awx/pull/9334
- Added user interface for management jobs: https://github.com/ansible/awx/pull/9224
- Added toast message to show notification template test result to notification templates list https://github.com/ansible/awx/pull/9318
+- Replaced CodeMirror with AceEditor for editing template variables and notification templates https://github.com/ansible/awx/pull/9281
# 17.1.0 (March 9th, 2021)
- Addressed a security issue in AWX (CVE-2021-20253)
diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json
index d4ed859ea9..daf0c1d466 100644
--- a/awx/ui_next/package-lock.json
+++ b/awx/ui_next/package-lock.json
@@ -2786,6 +2786,11 @@
"negotiator": "0.6.2"
}
},
+ "ace-builds": {
+ "version": "1.4.12",
+ "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.12.tgz",
+ "integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg=="
+ },
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@@ -4947,11 +4952,6 @@
"q": "^1.1.2"
}
},
- "codemirror": {
- "version": "5.58.3",
- "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.3.tgz",
- "integrity": "sha512-KBhB+juiyOOgn0AqtRmWyAT3yoElkuvWTI6hsHa9E6GQrl6bk/fdAYcvuqW1/upO9T9rtEtapWdw4XYcNiVDEA=="
- },
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -6230,6 +6230,11 @@
}
}
},
+ "diff-match-patch": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
+ "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
+ },
"diff-sequences": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz",
@@ -11295,11 +11300,15 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
+ "lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
+ },
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
- "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
- "dev": true
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.memoize": {
"version": "4.1.2",
@@ -14175,6 +14184,18 @@
"prop-types": "^15.6.2"
}
},
+ "react-ace": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.3.0.tgz",
+ "integrity": "sha512-RWPDwVobLvyD0wDoHHQqEnn9pNQBhMnmo6LmRACkaXxAg3UQZpse6x9JFLC5EXyWby+P3uolIlQPct4NFEBPNg==",
+ "requires": {
+ "ace-builds": "^1.4.6",
+ "diff-match-patch": "^1.0.4",
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "prop-types": "^15.7.2"
+ }
+ },
"react-app-polyfill": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz",
@@ -14203,11 +14224,6 @@
}
}
},
- "react-codemirror2": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-6.0.1.tgz",
- "integrity": "sha512-rutEKVgvFhWcy/GeVA1hFbqrO89qLqgqdhUr7YhYgIzdyICdlRQv+ztuNvOFQMXrO0fLt0VkaYOdMdYdQgsSUA=="
- },
"react-dev-utils": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",
diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json
index 24371abfc7..99c5a04d1d 100644
--- a/awx/ui_next/package.json
+++ b/awx/ui_next/package.json
@@ -11,9 +11,9 @@
"@patternfly/react-core": "^4.90.2",
"@patternfly/react-icons": "4.7.22",
"@patternfly/react-table": "^4.19.15",
+ "ace-builds": "^1.4.12",
"ansi-to-html": "^0.6.11",
"axios": "^0.21.1",
- "codemirror": "^5.47.0",
"d3": "^5.12.0",
"dagre": "^0.8.4",
"formik": "^2.1.2",
@@ -22,7 +22,7 @@
"js-yaml": "^3.13.1",
"prop-types": "^15.6.2",
"react": "^16.13.1",
- "react-codemirror2": "^6.0.0",
+ "react-ace": "^9.3.0",
"react-dom": "^16.13.1",
"react-router-dom": "^5.1.2",
"react-virtualized": "^9.21.1",
diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx
index 46a3617d7e..229024bfbe 100644
--- a/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx
+++ b/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx
@@ -10,7 +10,7 @@ import styled from 'styled-components';
import { BrandName } from '../../variables';
import AnsibleSelect from '../AnsibleSelect';
import FormField from '../FormField';
-import { VariablesField } from '../CodeMirrorInput';
+import { VariablesField } from '../CodeEditor';
import {
FormColumnLayout,
FormFullWidthLayout,
diff --git a/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx b/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx
new file mode 100644
index 0000000000..0218a83c3c
--- /dev/null
+++ b/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx
@@ -0,0 +1,193 @@
+import React, { useEffect, useRef, useCallback } from 'react';
+import { oneOf, bool, number, string, func } from 'prop-types';
+import ReactAce from 'react-ace';
+import 'ace-builds/src-noconflict/mode-json';
+import 'ace-builds/src-noconflict/mode-javascript';
+import 'ace-builds/src-noconflict/mode-yaml';
+import 'ace-builds/src-noconflict/mode-django';
+import 'ace-builds/src-noconflict/theme-github';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import styled from 'styled-components';
+import debounce from '../../util/debounce';
+
+const LINE_HEIGHT = 24;
+const PADDING = 12;
+
+const FocusWrapper = styled.div`
+ && + .keyboard-help-text {
+ opacity: 0;
+ transition: opacity 0.1s linear;
+ }
+
+ &:focus-within + .keyboard-help-text {
+ opacity: 1;
+ }
+
+ & .ace_hidden-cursors .ace_cursor {
+ opacity: 0;
+ }
+`;
+
+const AceEditor = styled(ReactAce)`
+ font-family: var(--pf-global--FontFamily--monospace);
+ max-height: 90vh;
+
+ & .ace_gutter,
+ & .ace_scroller {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ }
+
+ & .ace_mobile-menu {
+ display: none;
+ }
+
+ ${props =>
+ props.hasErrors &&
+ `
+ && {
+ --pf-c-form-control--PaddingRight: var(--pf-c-form-control--invalid--PaddingRight);
+ --pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--invalid--BorderBottomColor);
+ padding-right: 24px;
+ padding-bottom: var(--pf-c-form-control--invalid--PaddingBottom);
+ background: var(--pf-c-form-control--invalid--Background);
+ border-bottom-width: var(--pf-c-form-control--invalid--BorderBottomWidth);
+ }`}
+
+ ${props =>
+ props.setOptions.readOnly &&
+ `
+ && .ace_cursor {
+ opacity: 0;
+ }
+ `}
+`;
+AceEditor.displayName = 'AceEditor';
+
+function CodeEditor({
+ id,
+ value,
+ onChange,
+ mode,
+ readOnly,
+ hasErrors,
+ rows,
+ fullHeight,
+ className,
+ i18n,
+}) {
+ const wrapper = useRef(null);
+ const editor = useRef(null);
+
+ useEffect(
+ function removeTextareaTabIndex() {
+ const editorInput = editor.current.refEditor?.querySelector('textarea');
+ if (editorInput && !readOnly) {
+ editorInput.tabIndex = -1;
+ }
+ },
+ [readOnly]
+ );
+
+ const listen = useCallback(event => {
+ if (wrapper.current === document.activeElement && event.key === 'Enter') {
+ const editorInput = editor.current.refEditor?.querySelector('textarea');
+ if (!editorInput) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ editorInput.focus();
+ }
+ }, []);
+
+ useEffect(function addKeyEventListeners() {
+ const wrapperEl = wrapper.current;
+ wrapperEl.addEventListener('keydown', listen);
+
+ return () => {
+ wrapperEl.removeEventListener('keydown', listen);
+ };
+ });
+
+ const aceModes = {
+ javascript: 'json',
+ yaml: 'yaml',
+ jinja2: 'django',
+ };
+
+ const numRows = fullHeight ? value.split('\n').length : rows;
+
+ return (
+ <>
+
+ {
+ wrapper.current.focus();
+ },
+ },
+ {
+ name: 'tab escape',
+ bindKey: { win: 'Shift-Tab', mac: 'Shift-Tab' },
+ exec: () => {
+ wrapper.current.focus();
+ },
+ },
+ ]}
+ ref={editor}
+ />
+
+ {!readOnly && (
+
+ {i18n._(t`Press Enter to edit. Press ESC to stop editing.`)}
+
+ )}
+ >
+ );
+}
+CodeEditor.propTypes = {
+ value: string.isRequired,
+ onChange: func,
+ mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
+ readOnly: bool,
+ hasErrors: bool,
+ fullHeight: bool,
+ rows: number,
+ className: string,
+};
+CodeEditor.defaultProps = {
+ readOnly: false,
+ onChange: () => {},
+ rows: 6,
+ fullHeight: false,
+ hasErrors: false,
+ className: '',
+};
+
+export default withI18n()(CodeEditor);
diff --git a/awx/ui_next/src/components/CodeEditor/CodeEditor.test.jsx b/awx/ui_next/src/components/CodeEditor/CodeEditor.test.jsx
new file mode 100644
index 0000000000..69f498d2e3
--- /dev/null
+++ b/awx/ui_next/src/components/CodeEditor/CodeEditor.test.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import CodeEditor from './CodeEditor';
+import debounce from '../../util/debounce';
+
+jest.mock('../../util/debounce');
+
+describe('CodeEditor', () => {
+ beforeEach(() => {
+ document.body.createTextRange = jest.fn();
+ });
+
+ it('should pass value and mode through to ace editor', () => {
+ const onChange = jest.fn();
+ const wrapper = mountWithContexts(
+
+ );
+ const aceEditor = wrapper.find('AceEditor');
+ expect(aceEditor.prop('mode')).toEqual('yaml');
+ expect(aceEditor.prop('setOptions').readOnly).toEqual(false);
+ expect(aceEditor.prop('value')).toEqual('---\nfoo: bar');
+ });
+
+ it('should trigger onChange prop', () => {
+ debounce.mockImplementation(fn => fn);
+ const onChange = jest.fn();
+ const wrapper = mountWithContexts(
+
+ );
+ const aceEditor = wrapper.find('AceEditor');
+ aceEditor.prop('onChange')('newvalue');
+ expect(onChange).toHaveBeenCalledWith('newvalue');
+ });
+
+ it('should render in read only mode', () => {
+ const onChange = jest.fn();
+ const wrapper = mountWithContexts(
+
+ );
+ const aceEditor = wrapper.find('AceEditor');
+ expect(aceEditor.prop('setOptions').readOnly).toEqual(true);
+ expect(aceEditor.prop('value')).toEqual('---');
+ });
+});
diff --git a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorField.jsx b/awx/ui_next/src/components/CodeEditor/CodeEditorField.jsx
similarity index 86%
rename from awx/ui_next/src/components/CodeMirrorInput/CodeMirrorField.jsx
rename to awx/ui_next/src/components/CodeEditor/CodeEditorField.jsx
index 35ebf32721..61dec6a04f 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorField.jsx
+++ b/awx/ui_next/src/components/CodeEditor/CodeEditorField.jsx
@@ -11,10 +11,10 @@ import {
} from 'prop-types';
import { useField } from 'formik';
import { FormGroup } from '@patternfly/react-core';
-import CodeMirrorInput from './CodeMirrorInput';
+import CodeEditor from './CodeEditor';
import Popover from '../Popover';
-function CodeMirrorField({
+function CodeEditorField({
id,
name,
label,
@@ -39,7 +39,7 @@ function CodeMirrorField({
label={label}
labelIcon={}
>
-
);
}
-CodeMirrorField.propTypes = {
+CodeEditorField.propTypes = {
helperText: string,
id: string.isRequired,
name: string.isRequired,
@@ -63,7 +63,7 @@ CodeMirrorField.propTypes = {
rows: number,
};
-CodeMirrorField.defaultProps = {
+CodeEditorField.defaultProps = {
helperText: '',
validate: () => {},
isRequired: false,
@@ -71,4 +71,4 @@ CodeMirrorField.defaultProps = {
rows: 5,
};
-export default CodeMirrorField;
+export default CodeEditorField;
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx
similarity index 98%
rename from awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
rename to awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx
index b5501fb0b7..f5cfd91373 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
+++ b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx
@@ -12,7 +12,7 @@ import {
isJsonObject,
isJsonString,
} from '../../util/yaml';
-import CodeMirrorInput from './CodeMirrorInput';
+import CodeEditor from './CodeEditor';
import { JSON_MODE, YAML_MODE } from './constants';
function getValueAsMode(value, mode) {
@@ -99,7 +99,7 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, fullHeight }) {
fullWidth
css="grid-column: 1 / -1; margin-top: -20px"
>
- ', () => {
- test('should render readonly CodeMirrorInput', () => {
+ test('should render readonly CodeEditor', () => {
const wrapper = mountWithContexts(
);
- const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
+ const input = wrapper.find('VariablesDetail___StyledCodeEditor');
expect(input).toHaveLength(1);
expect(input.prop('mode')).toEqual('yaml');
expect(input.prop('value')).toEqual('---foo: bar');
@@ -21,7 +21,7 @@ describe('', () => {
const wrapper = mountWithContexts(
);
- const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
+ const input = wrapper.find('VariablesDetail___StyledCodeEditor');
expect(input).toHaveLength(1);
expect(input.prop('mode')).toEqual('javascript');
expect(input.prop('value')).toEqual('{"foo": "bar"}');
@@ -32,12 +32,12 @@ describe('', () => {
);
wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
- const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
+ const input = wrapper.find('VariablesDetail___StyledCodeEditor');
expect(input.prop('mode')).toEqual('javascript');
expect(input.prop('value')).toEqual('{\n "foo": "bar"\n}');
wrapper.find('MultiButtonToggle').invoke('onChange')('yaml');
- const input2 = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
+ const input2 = wrapper.find('VariablesDetail___StyledCodeEditor');
expect(input2.prop('mode')).toEqual('yaml');
expect(input2.prop('value')).toEqual('foo: bar\n');
});
@@ -46,9 +46,7 @@ describe('', () => {
const wrapper = mountWithContexts(
);
- expect(wrapper.find('VariablesDetail___StyledCodeMirrorInput').length).toBe(
- 1
- );
+ expect(wrapper.find('VariablesDetail___StyledCodeEditor').length).toBe(1);
expect(wrapper.find('div.pf-c-form__label').text()).toBe('Variables');
});
@@ -63,7 +61,7 @@ describe('', () => {
value: '---bar: baz',
});
wrapper.update();
- const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
+ const input = wrapper.find('VariablesDetail___StyledCodeEditor');
expect(input.prop('mode')).toEqual('javascript');
expect(input.prop('value')).toEqual('{\n "bar": "baz"\n}');
});
@@ -72,7 +70,7 @@ describe('', () => {
const wrapper = mountWithContexts(
);
- const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
+ const input = wrapper.find('VariablesDetail___StyledCodeEditor');
expect(input.prop('value')).toEqual('---');
});
@@ -84,7 +82,7 @@ describe('', () => {
wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
});
wrapper.setProps({ value: '' });
- const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
+ const input = wrapper.find('VariablesDetail___StyledCodeEditor');
expect(input.prop('value')).toEqual('{}');
});
});
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx
similarity index 97%
rename from awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
rename to awx/ui_next/src/components/CodeEditor/VariablesField.jsx
index 1db21fdf04..8fbf1e4bfa 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
+++ b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx
@@ -8,7 +8,7 @@ import { Split, SplitItem } from '@patternfly/react-core';
import { CheckboxField } from '../FormField';
import MultiButtonToggle from '../MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml';
-import CodeMirrorInput from './CodeMirrorInput';
+import CodeEditor from './CodeEditor';
import Popover from '../Popover';
import { JSON_MODE, YAML_MODE } from './constants';
@@ -76,7 +76,7 @@ function VariablesField({
/>
)}
- {
document.body.createTextRange = jest.fn();
});
- it('should render code mirror input', () => {
+ it('should render code editor', () => {
const value = '---\n';
const wrapper = mountWithContexts(
@@ -18,12 +18,12 @@ describe('VariablesField', () => {
)}
);
- const codemirror = wrapper.find('Controlled');
- expect(codemirror.prop('value')).toEqual(value);
+ const codeEditor = wrapper.find('CodeEditor');
+ expect(codeEditor.prop('value')).toEqual(value);
});
- it('should render yaml/json toggles', async () => {
- const value = '---\n';
+ it('should toggle between yaml/json', async () => {
+ const value = '---\nfoo: bar\nbaz: 3';
const wrapper = mountWithContexts(
{() => (
@@ -39,7 +39,10 @@ describe('VariablesField', () => {
buttons.at(1).simulate('click');
});
wrapper.update();
- expect(wrapper.find('CodeMirrorInput').prop('mode')).toEqual('javascript');
+ expect(wrapper.find('CodeEditor').prop('mode')).toEqual('javascript');
+ expect(wrapper.find('CodeEditor').prop('value')).toEqual(
+ '{\n "foo": "bar",\n "baz": 3\n}'
+ );
const buttons2 = wrapper.find('Button');
expect(buttons2.at(0).prop('variant')).toEqual('secondary');
expect(buttons2.at(1).prop('variant')).toEqual('primary');
@@ -47,7 +50,10 @@ describe('VariablesField', () => {
buttons2.at(0).simulate('click');
});
wrapper.update();
- expect(wrapper.find('CodeMirrorInput').prop('mode')).toEqual('yaml');
+ expect(wrapper.find('CodeEditor').prop('mode')).toEqual('yaml');
+ expect(wrapper.find('CodeEditor').prop('value')).toEqual(
+ 'foo: bar\nbaz: 3\n'
+ );
});
it('should set Formik error if yaml is invalid', async () => {
@@ -65,10 +71,11 @@ describe('VariablesField', () => {
.simulate('click');
wrapper.update();
- const field = wrapper.find('CodeMirrorInput');
+ const field = wrapper.find('CodeEditor');
expect(field.prop('hasErrors')).toEqual(true);
expect(wrapper.find('.pf-m-error')).toHaveLength(1);
});
+
it('should render tooltip', () => {
const value = '---\n';
const wrapper = mountWithContexts(
@@ -102,9 +109,7 @@ describe('VariablesField', () => {
);
await act(async () => {
- wrapper.find('CodeMirrorInput').invoke('onChange')(
- '---\nnewval: changed'
- );
+ wrapper.find('CodeEditor').invoke('onChange')('---\nnewval: changed');
wrapper.find('form').simulate('submit');
});
@@ -129,6 +134,6 @@ describe('VariablesField', () => {
);
- expect(wrapper.find('CodeMirrorInput').prop('mode')).toEqual('javascript');
+ expect(wrapper.find('CodeEditor').prop('mode')).toEqual('javascript');
});
});
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeEditor/VariablesInput.jsx
similarity index 97%
rename from awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
rename to awx/ui_next/src/components/CodeEditor/VariablesInput.jsx
index a43962bd76..354e263b69 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
+++ b/awx/ui_next/src/components/CodeEditor/VariablesInput.jsx
@@ -4,7 +4,7 @@ import { Split, SplitItem } from '@patternfly/react-core';
import styled from 'styled-components';
import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml';
import MultiButtonToggle from '../MultiButtonToggle';
-import CodeMirrorInput from './CodeMirrorInput';
+import CodeEditor from './CodeEditor';
import { JSON_MODE, YAML_MODE } from './constants';
function formatJson(jsonString) {
@@ -63,7 +63,7 @@ function VariablesInput(props) {
/>
- .CodeMirror {
- height: ${props =>
- props.fullHeight ? 'auto' : `${props.rows * LINE_HEIGHT + PADDING}px`};
- font-family: var(--pf-global--FontFamily--monospace);
- }
-
- ${props =>
- props.hasErrors &&
- `
- && {
- --pf-c-form-control--PaddingRight: var(--pf-c-form-control--invalid--PaddingRight);
- --pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--invalid--BorderBottomColor);
- padding-right: 24px;
- padding-bottom: var(--pf-c-form-control--invalid--PaddingBottom);
- background: var(--pf-c-form-control--invalid--Background);
- border-bottom-width: var(--pf-c-form-control--invalid--BorderBottomWidth);
- }`}
-
- ${props =>
- props.options &&
- props.options.readOnly &&
- `
- &,
- &:hover {
- --pf-c-form-control--BorderBottomColor: var(--pf-global--BorderColor--300);
- }
-
- .CodeMirror-cursors {
- display: none;
- }
-
- .CodeMirror-lines {
- cursor: default;
- }
-
- .CodeMirror-scroll {
- background-color: var(--pf-c-form-control--disabled--BackgroundColor);
- }
- `}
- ${props =>
- props.options &&
- props.options.placeholder &&
- `
- .CodeMirror-empty {
- pre.CodeMirror-placeholder {
- color: var(--pf-c-form-control--placeholder--Color);
- height: 100% !important;
- }
- }
- `}
-`;
-
-function CodeMirrorInput({
- value,
- onChange,
- mode,
- readOnly,
- hasErrors,
- rows,
- fullHeight,
- className,
- placeholder,
-}) {
- // Workaround for CodeMirror bug: If CodeMirror renders in a modal on the
- // modal's initial render, it appears as an empty box due to mis-calculated
- // element height. Forcing an initial render before mounting
- // fixes this.
- const [isInitialized, setIsInitialized] = useState(false);
- useEffect(() => {
- if (!isInitialized) {
- setIsInitialized(true);
- }
- }, [isInitialized]);
- if (!isInitialized) {
- return ;
- }
-
- return (
- onChange(val)}
- mode={mode}
- hasErrors={hasErrors}
- options={{
- smartIndent: false,
- lineNumbers: true,
- lineWrapping: true,
- placeholder,
- readOnly,
- }}
- fullHeight={fullHeight}
- rows={rows}
- />
- );
-}
-CodeMirrorInput.propTypes = {
- value: string.isRequired,
- onChange: func,
- mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
- readOnly: bool,
- hasErrors: bool,
- fullHeight: bool,
- rows: number,
- className: string,
-};
-CodeMirrorInput.defaultProps = {
- readOnly: false,
- onChange: () => {},
- rows: 6,
- fullHeight: false,
- hasErrors: false,
- className: '',
-};
-
-export default CodeMirrorInput;
diff --git a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.test.jsx b/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.test.jsx
deleted file mode 100644
index aaf9d33118..0000000000
--- a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.test.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import CodeMirrorInput from './CodeMirrorInput';
-
-describe('CodeMirrorInput', () => {
- beforeEach(() => {
- document.body.createTextRange = jest.fn();
- });
-
- it('should trigger onChange prop', () => {
- const onChange = jest.fn();
- const wrapper = mount(
-
- );
- const codemirror = wrapper.find('Controlled');
- expect(codemirror.prop('mode')).toEqual('yaml');
- expect(codemirror.prop('options').readOnly).toEqual(false);
- codemirror.prop('onBeforeChange')(null, null, 'newvalue');
- expect(onChange).toHaveBeenCalledWith('newvalue');
- });
-
- it('should render in read only mode', () => {
- const onChange = jest.fn();
- const wrapper = mount(
-
- );
- const codemirror = wrapper.find('Controlled');
- expect(codemirror.prop('options').readOnly).toEqual(true);
- });
-});
diff --git a/awx/ui_next/src/components/DetailList/CodeDetail.jsx b/awx/ui_next/src/components/DetailList/CodeDetail.jsx
index 6d78ef043a..08c935b18e 100644
--- a/awx/ui_next/src/components/DetailList/CodeDetail.jsx
+++ b/awx/ui_next/src/components/DetailList/CodeDetail.jsx
@@ -11,7 +11,7 @@ import {
} from 'prop-types';
import { TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from './Detail';
-import CodeMirrorInput from '../CodeMirrorInput';
+import CodeEditor from '../CodeEditor';
import Popover from '../Popover';
function CodeDetail({
@@ -52,7 +52,7 @@ function CodeDetail({
css="grid-column: 1 / -1; margin-top: -20px"
data-cy={valueCy}
>
- ', () => {
expect(sshKeyUnlockDetail.length).toBe(1);
expect(sshKeyUnlockDetail.find('CredentialChip').length).toBe(1);
expect(
- wrapper.find('CodeMirrorInput#credential-ssh_key_unlock-metadata').props()
+ wrapper.find('CodeEditor#credential-ssh_key_unlock-metadata').props()
.value
).toBe(JSON.stringify(mockInputSource.metadata, null, 2));
expectDetailToMatch(
diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx
index 1b99c16867..0cdf678d67 100644
--- a/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx
+++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx
@@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
import { Link, useHistory } from 'react-router-dom';
import { Button } from '@patternfly/react-core';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
import DeleteButton from '../../../components/DeleteButton';
diff --git a/awx/ui_next/src/screens/CredentialType/shared/CredentialTypeForm.jsx b/awx/ui_next/src/screens/CredentialType/shared/CredentialTypeForm.jsx
index 8bb5f113ef..a5e9c540a9 100644
--- a/awx/ui_next/src/screens/CredentialType/shared/CredentialTypeForm.jsx
+++ b/awx/ui_next/src/screens/CredentialType/shared/CredentialTypeForm.jsx
@@ -5,7 +5,7 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Form } from '@patternfly/react-core';
-import { VariablesField } from '../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../components/CodeEditor';
import FormField, { FormSubmitError } from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup';
import { required } from '../../../util/validators';
diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx
index ae140d3786..aecb17a53e 100644
--- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx
+++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx
@@ -13,7 +13,7 @@ import {
Detail,
UserDateDetail,
} from '../../../components/DetailList';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import Sparkline from '../../../components/Sparkline';
import DeleteButton from '../../../components/DeleteButton';
import { HostsAPI } from '../../../api';
diff --git a/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx b/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx
index 1a107ab28e..f33c989f5b 100644
--- a/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx
+++ b/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx
@@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
import { Host } from '../../../types';
import { CardBody } from '../../../components/Card';
import { DetailList } from '../../../components/DetailList';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import useRequest from '../../../util/useRequest';
diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx
index 6cce625c78..6cf0bedf87 100644
--- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx
+++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx
@@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
import { Link, useHistory } from 'react-router-dom';
import { Button, Label } from '@patternfly/react-core';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
import DeleteButton from '../../../components/DeleteButton';
diff --git a/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx b/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx
index e41ab0a1b0..719c907d1b 100644
--- a/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx
+++ b/awx/ui_next/src/screens/InstanceGroup/shared/ContainerGroupForm.jsx
@@ -19,7 +19,7 @@ import {
SubFormLayout,
} from '../../../components/FormLayout';
import CredentialLookup from '../../../components/Lookup/CredentialLookup';
-import { VariablesField } from '../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../components/CodeEditor';
function ContainerGroupFormFields({ i18n, instanceGroup }) {
const { setFieldValue } = useFormikContext();
diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
index 09dfe7ed54..ec441d7390 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
@@ -10,7 +10,7 @@ import {
Detail,
UserDateDetail,
} from '../../../components/DetailList';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail';
import ContentError from '../../../components/ContentError';
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx
index f8ce4879c8..8a4fa92fdf 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx
@@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { useHistory, useParams } from 'react-router-dom';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import { CardBody, CardActionsRow } from '../../../components/Card';
import ErrorDetail from '../../../components/ErrorDetail';
import AlertModal from '../../../components/AlertModal';
diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.jsx
index a2b48fecd1..c90c20885f 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.jsx
@@ -13,7 +13,7 @@ import {
Detail,
UserDateDetail,
} from '../../../components/DetailList';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import Sparkline from '../../../components/Sparkline';
import DeleteButton from '../../../components/DeleteButton';
import { HostsAPI } from '../../../api';
diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx
index 2f24b4cb38..6bffd37ba4 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx
@@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
import { Host } from '../../../types';
import { CardBody } from '../../../components/Card';
import { DetailList } from '../../../components/DetailList';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import useRequest from '../../../util/useRequest';
diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
index 4383998994..efc2a8ff73 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
@@ -6,7 +6,7 @@ import { t } from '@lingui/macro';
import { Button, List, ListItem } from '@patternfly/react-core';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import CredentialChip from '../../../components/CredentialChip';
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx
index 8aee3165e5..fa29c0ad61 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx
@@ -12,7 +12,7 @@ import useRequest, { useDismissableError } from '../../../util/useRequest';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
import ChipGroup from '../../../components/ChipGroup';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import DeleteButton from '../../../components/DeleteButton';
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryEdit/SmartInventoryEdit.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryEdit/SmartInventoryEdit.test.jsx
index 90ed4abb6f..789d8067c9 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryEdit/SmartInventoryEdit.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryEdit/SmartInventoryEdit.test.jsx
@@ -74,6 +74,10 @@ describe('', () => {
wrapper.unmount();
});
+ test('should render CodeEditor field', () => {
+ expect(wrapper.find('CodeEditor').prop('value')).toEqual('---');
+ });
+
test('should fetch related instance groups on initial render', async () => {
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
});
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx
index ca992575b1..3abb48d0ca 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx
@@ -10,7 +10,7 @@ import {
UserDateDetail,
} from '../../../components/DetailList';
import Sparkline from '../../../components/Sparkline';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
function SmartInventoryHostDetail({ host, i18n }) {
const {
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx
index 0cce85e9ca..c02edb8ea5 100644
--- a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx
@@ -5,7 +5,7 @@ import { t } from '@lingui/macro';
import { func, number, shape } from 'prop-types';
import { Form } from '@patternfly/react-core';
-import { VariablesField } from '../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../components/CodeEditor';
import FormField, { FormSubmitError } from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup';
import { required } from '../../../util/validators';
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx
index 86ea72e52c..dc2b5bd246 100644
--- a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx
@@ -94,6 +94,7 @@ describe('', () => {
1
);
expect(wrapper.find('VariablesField[label="Variables"]').length).toBe(1);
+ expect(wrapper.find('CodeEditor').prop('value')).toEqual('---');
});
test('should update form values', async () => {
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx
index 5fa41a2921..ac894d918b 100644
--- a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx
@@ -7,7 +7,7 @@ import { t } from '@lingui/macro';
import { CardBody } from '../../../components/Card';
import FormField, { FormSubmitError } from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
-import { VariablesField } from '../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../components/CodeEditor';
import { required } from '../../../util/validators';
import {
FormColumnLayout,
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx
index 6501b2bf4b..31f5a4f12e 100644
--- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx
@@ -5,7 +5,7 @@ import { useField } from 'formik';
import { FormGroup } from '@patternfly/react-core';
import { minMaxValue, regExp } from '../../../../util/validators';
import AnsibleSelect from '../../../../components/AnsibleSelect';
-import { VariablesField } from '../../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../../components/CodeEditor';
import FormField, { CheckboxField } from '../../../../components/FormField';
import {
FormFullWidthLayout,
diff --git a/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx b/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx
index a851fbfdc4..fca698aef2 100644
--- a/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx
@@ -6,7 +6,7 @@ import { useLocation } from 'react-router-dom';
import { func, shape, arrayOf } from 'prop-types';
import { Form } from '@patternfly/react-core';
import { InstanceGroup } from '../../../types';
-import { VariablesField } from '../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../components/CodeEditor';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import FormActionGroup from '../../../components/FormActionGroup';
diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
index ad0be4c9e6..176ca95556 100644
--- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
+++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
@@ -16,7 +16,7 @@ import {
import { CardBody, CardActionsRow } from '../../../components/Card';
import ChipGroup from '../../../components/ChipGroup';
import CredentialChip from '../../../components/CredentialChip';
-import { VariablesInput as _VariablesInput } from '../../../components/CodeMirrorInput';
+import { VariablesInput as _VariablesInput } from '../../../components/CodeEditor';
import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail';
import {
diff --git a/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx b/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx
index f82e12eb00..20f363a849 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx
@@ -8,7 +8,7 @@ import { AllHtmlEntities } from 'html-entities';
import StatusIcon from '../../../components/StatusIcon';
import { DetailList, Detail } from '../../../components/DetailList';
import ContentEmpty from '../../../components/ContentEmpty';
-import CodeMirrorInput from '../../../components/CodeMirrorInput';
+import CodeEditor from '../../../components/CodeEditor';
const entities = new AllHtmlEntities();
@@ -45,18 +45,18 @@ const processEventStatus = event => {
return status;
};
-const processCodeMirrorValue = value => {
- let codeMirrorValue;
+const processCodeEditorValue = value => {
+ let codeEditorValue;
if (value === undefined) {
- codeMirrorValue = false;
+ codeEditorValue = false;
} else if (value === '') {
- codeMirrorValue = ' ';
+ codeEditorValue = ' ';
} else if (typeof value === 'string') {
- codeMirrorValue = entities.encode(value);
+ codeEditorValue = entities.encode(value);
} else {
- codeMirrorValue = value;
+ codeEditorValue = value;
}
- return codeMirrorValue;
+ return codeEditorValue;
};
const processStdOutValue = hostEvent => {
@@ -90,9 +90,9 @@ function HostEventModal({ onClose, hostEvent = {}, isOpen = false, i18n }) {
setActiveTabKey(tabIndex);
};
- const jsonObj = processCodeMirrorValue(hostEvent?.event_data?.res);
- const stdErr = processCodeMirrorValue(hostEvent?.event_data?.res?.stderr);
- const stdOut = processCodeMirrorValue(processStdOutValue(hostEvent));
+ const jsonObj = processCodeEditorValue(hostEvent?.event_data?.res);
+ const stdErr = processCodeEditorValue(hostEvent?.event_data?.res?.stderr);
+ const stdOut = processCodeEditorValue(processStdOutValue(hostEvent));
return (
{activeTabKey === 1 && jsonObj ? (
-
{activeTabKey === 2 && stdOut ? (
-
{activeTabKey === 3 && stdErr ? (
- {}}
diff --git a/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.test.jsx b/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.test.jsx
index bb4b538142..3fe04f05a8 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.test.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.test.jsx
@@ -188,12 +188,12 @@ describe('HostEventModal', () => {
expect(jsonSection.find('EmptyState').length).toBe(1);
wrapper.find('button[aria-label="JSON tab"]').simulate('click');
findSections(wrapper);
- expect(jsonSection.find('CodeMirrorInput').length).toBe(1);
+ expect(jsonSection.find('CodeEditor').length).toBe(1);
- const codemirror = jsonSection.find('CodeMirrorInput Controlled');
- expect(codemirror.prop('mode')).toBe('javascript');
- expect(codemirror.prop('options').readOnly).toBe(true);
- expect(codemirror.prop('value')).toEqual(jsonValue);
+ const codeEditor = jsonSection.find('CodeEditor');
+ expect(codeEditor.prop('mode')).toBe('javascript');
+ expect(codeEditor.prop('readOnly')).toBe(true);
+ expect(codeEditor.prop('value')).toEqual(jsonValue);
});
test('should display Standard Out tab content on tab click', () => {
@@ -205,12 +205,12 @@ describe('HostEventModal', () => {
expect(standardOutSection.find('EmptyState').length).toBe(1);
wrapper.find('button[aria-label="Standard out tab"]').simulate('click');
findSections(wrapper);
- expect(standardOutSection.find('CodeMirrorInput').length).toBe(1);
+ expect(standardOutSection.find('CodeEditor').length).toBe(1);
- const codemirror = standardOutSection.find('CodeMirrorInput Controlled');
- expect(codemirror.prop('mode')).toBe('javascript');
- expect(codemirror.prop('options').readOnly).toBe(true);
- expect(codemirror.prop('value')).toEqual(hostEvent.event_data.res.stdout);
+ const codeEditor = standardOutSection.find('CodeEditor');
+ expect(codeEditor.prop('mode')).toBe('javascript');
+ expect(codeEditor.prop('readOnly')).toBe(true);
+ expect(codeEditor.prop('value')).toEqual(hostEvent.event_data.res.stdout);
});
test('should display Standard Error tab content on tab click', () => {
@@ -229,12 +229,12 @@ describe('HostEventModal', () => {
expect(standardErrorSection.find('EmptyState').length).toBe(1);
wrapper.find('button[aria-label="Standard error tab"]').simulate('click');
findSections(wrapper);
- expect(standardErrorSection.find('CodeMirrorInput').length).toBe(1);
+ expect(standardErrorSection.find('CodeEditor').length).toBe(1);
- const codemirror = standardErrorSection.find('CodeMirrorInput Controlled');
- expect(codemirror.prop('mode')).toBe('javascript');
- expect(codemirror.prop('options').readOnly).toBe(true);
- expect(codemirror.prop('value')).toEqual(' ');
+ const codeEditor = standardErrorSection.find('CodeEditor');
+ expect(codeEditor.prop('mode')).toBe('javascript');
+ expect(codeEditor.prop('readOnly')).toBe(true);
+ expect(codeEditor.prop('value')).toEqual(' ');
});
test('should call onClose when close button is clicked', () => {
@@ -263,8 +263,8 @@ describe('HostEventModal', () => {
);
wrapper.find('button[aria-label="Standard out tab"]').simulate('click');
findSections(wrapper);
- const codemirror = standardOutSection.find('CodeMirrorInput Controlled');
- expect(codemirror.prop('value')).toEqual('foo bar');
+ const codeEditor = standardOutSection.find('CodeEditor');
+ expect(codeEditor.prop('value')).toEqual('foo bar');
});
test('should render standard out of yum task', () => {
@@ -282,7 +282,7 @@ describe('HostEventModal', () => {
);
wrapper.find('button[aria-label="Standard out tab"]').simulate('click');
findSections(wrapper);
- const codemirror = standardOutSection.find('CodeMirrorInput Controlled');
- expect(codemirror.prop('value')).toEqual('baz');
+ const codeEditor = standardOutSection.find('CodeEditor');
+ expect(codeEditor.prop('value')).toEqual('baz');
});
});
diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx
index b452ed4b62..fca1b0317d 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx
@@ -8,7 +8,7 @@ import {
FormFullWidthLayout,
SubFormLayout,
} from '../../../components/FormLayout';
-import CodeMirrorField from '../../../components/CodeMirrorInput/CodeMirrorField';
+import CodeEditorField from '../../../components/CodeEditor/CodeEditorField';
function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
const [useCustomField, , useCustomHelpers] = useField('useCustomMessages');
@@ -102,7 +102,7 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
{showMessages && (
-
)}
{showBodies && (
-
)}
{showMessages && (
-
)}
{showBodies && (
-
)}
{showMessages && (
-
)}
{showBodies && (
-
)}
{showMessages && (
-
)}
{showBodies && (
-
)}
{showMessages && (
-
)}
{showBodies && (
-
)}
{showMessages && (
-
)}
{showBodies && (
-
)}
{showMessages && (
-
)}
{showBodies && (
- ', () => {
).toEqual(defaultMessages);
});
+ test('should render custom messages fields', () => {
+ const wrapper = mountWithContexts(
+
+ );
+
+ expect(
+ wrapper
+ .find('CodeEditor')
+ .at(0)
+ .prop('value')
+ ).toEqual('Started');
+ });
+
test('should submit', async () => {
const handleSubmit = jest.fn();
const wrapper = mountWithContexts(
diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx
index 97b1dcc76c..6d5851254b 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx
@@ -14,7 +14,7 @@ import FormField, {
ArrayTextField,
} from '../../../components/FormField';
import AnsibleSelect from '../../../components/AnsibleSelect';
-import { CodeMirrorField } from '../../../components/CodeMirrorInput';
+import { CodeEditorField } from '../../../components/CodeEditor';
import {
combine,
required,
@@ -470,7 +470,7 @@ function WebhookFields({ i18n }) {
name="notification_configuration.disable_ssl_verification"
/>
- ', () => {
target: { value: 'new key', name: 'SOCIAL_AUTH_GITHUB_KEY' },
});
wrapper
- .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP')
+ .find('CodeEditor#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP')
.invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx
index f0bb46ab23..1ed4336511 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx
@@ -122,7 +122,7 @@ describe('', () => {
},
});
wrapper
- .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP')
+ .find('CodeEditor#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP')
.invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx
index 278dd5d37e..9c3d3bd344 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx
@@ -135,9 +135,7 @@ describe('', () => {
},
});
wrapper
- .find(
- 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP'
- )
+ .find('CodeEditor#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP')
.invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx
index 764c9fa377..a87782a81d 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx
@@ -129,9 +129,7 @@ describe('', () => {
},
});
wrapper
- .find(
- 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP'
- )
+ .find('CodeEditor#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP')
.invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx
index 57396c43d1..21f6e4180d 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx
@@ -113,7 +113,7 @@ describe('', () => {
target: { value: 'new org', name: 'SOCIAL_AUTH_GITHUB_ORG_NAME' },
});
wrapper
- .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP')
+ .find('CodeEditor#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP')
.invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx
index bbc36f8948..9c9e971cf1 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx
@@ -108,7 +108,7 @@ describe('', () => {
target: { value: '12345', name: 'SOCIAL_AUTH_GITHUB_TEAM_ID' },
});
wrapper
- .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP')
+ .find('CodeEditor#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP')
.invoke('onChange')('{\n"Default":{\n"users":\ntrue\n}\n}');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx
index 68a292232c..c01dae3201 100644
--- a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx
@@ -122,7 +122,7 @@ describe('', () => {
target: { value: 'new key', name: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY' },
});
wrapper
- .find('CodeMirrorInput#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP')
+ .find('CodeEditor#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP')
.invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx b/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx
index 71f998e341..e41e0c26e3 100644
--- a/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx
@@ -153,7 +153,7 @@ describe('', () => {
name: 'AUTH_LDAP_SERVER_URI',
},
});
- wrapper.find('CodeMirrorInput#AUTH_LDAP_TEAM_MAP').invoke('onChange')(
+ wrapper.find('CodeEditor#AUTH_LDAP_TEAM_MAP').invoke('onChange')(
'{\n"LDAP Sales":{\n"organization":\n"mock org"\n}\n}'
);
});
diff --git a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
index 59474acd8c..b5e118088a 100644
--- a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
+++ b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
@@ -14,7 +14,7 @@ import {
import FileUploadIcon from '@patternfly/react-icons/dist/js/icons/file-upload-icon';
import styled from 'styled-components';
import AnsibleSelect from '../../../components/AnsibleSelect';
-import CodeMirrorInput from '../../../components/CodeMirrorInput';
+import CodeEditor from '../../../components/CodeEditor';
import { PasswordInput } from '../../../components/FormField';
import { FormFullWidthLayout } from '../../../components/FormLayout';
import Popover from '../../../components/Popover';
@@ -284,7 +284,7 @@ const ObjectField = withI18n()(({ i18n, name, config, isRequired = false }) => {
popoverContent={config.help_text}
validated={isValid ? 'default' : 'error'}
>
- {
)}
);
- expect(wrapper.find('CodeMirrorInput')).toHaveLength(1);
- expect(wrapper.find('CodeMirrorInput').prop('value')).toBe(
+ expect(wrapper.find('CodeEditor')).toHaveLength(1);
+ expect(wrapper.find('CodeEditor').prop('value')).toBe(
'["one", "two", "three"]'
);
await act(async () => {
- wrapper.find('CodeMirrorInput').invoke('onChange')('[]');
+ wrapper.find('CodeEditor').invoke('onChange')('[]');
});
wrapper.update();
- expect(wrapper.find('CodeMirrorInput').prop('value')).toBe('[]');
+ expect(wrapper.find('CodeEditor').prop('value')).toBe('[]');
});
test('FileUploadField renders the expected content', async () => {
diff --git a/awx/ui_next/src/screens/Setting/shared/settingTestUtils.js b/awx/ui_next/src/screens/Setting/shared/settingTestUtils.js
index ab81162984..af2da46345 100644
--- a/awx/ui_next/src/screens/Setting/shared/settingTestUtils.js
+++ b/awx/ui_next/src/screens/Setting/shared/settingTestUtils.js
@@ -8,6 +8,6 @@ export function assertVariableDetail(wrapper, label, value) {
wrapper.find(`CodeDetail[label="${label}"] .pf-c-form__label`).text()
).toBe(label);
expect(
- wrapper.find(`CodeDetail[label="${label}"] CodeMirrorInput`).prop('value')
+ wrapper.find(`CodeDetail[label="${label}"] CodeEditor`).prop('value')
).toBe(value);
}
diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
index 4a2b47605f..d61819adcc 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
@@ -27,7 +27,7 @@ import {
import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail';
import { LaunchButton } from '../../../components/LaunchButton';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import { JobTemplatesAPI } from '../../../api';
import useRequest, { useDismissableError } from '../../../util/useRequest';
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
index 4f8237e8c2..7d21bafb3f 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
@@ -16,7 +16,7 @@ import { WorkflowJobTemplatesAPI } from '../../../api';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
import ChipGroup from '../../../components/ChipGroup';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
+import { VariablesDetail } from '../../../components/CodeEditor';
import DeleteButton from '../../../components/DeleteButton';
import {
DetailList,
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index b1ba6aac3d..e28b12ef47 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -29,7 +29,7 @@ import {
FormCheckboxLayout,
SubFormLayout,
} from '../../../components/FormLayout';
-import { VariablesField } from '../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../components/CodeEditor';
import { required } from '../../../util/validators';
import { JobTemplate } from '../../../types';
import {
diff --git a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx
index 5ee34bbe12..6108b0c8f7 100644
--- a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx
@@ -26,7 +26,7 @@ import {
InventoryLookup,
ExecutionEnvironmentLookup,
} from '../../../components/Lookup';
-import { VariablesField } from '../../../components/CodeMirrorInput';
+import { VariablesField } from '../../../components/CodeEditor';
import FormActionGroup from '../../../components/FormActionGroup';
import ContentError from '../../../components/ContentError';
import CheckboxField from '../../../components/FormField/CheckboxField';