Skip to content

Commit

Permalink
Merge pull request #9281 from keithjgrant/8905-codemirror-replacement
Browse files Browse the repository at this point in the history
AceEditor - codemirror replacement

Reviewed-by: John Hill <johill@redhat.com>
             https://github.com/unlikelyzero
  • Loading branch information
softwarefactory-project-zuul[bot] committed Mar 12, 2021
2 parents 9342cb0 + c90dfbb commit 8ba9eef
Show file tree
Hide file tree
Showing 67 changed files with 442 additions and 322 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 28 additions & 12 deletions awx/ui_next/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions awx/ui_next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
193 changes: 193 additions & 0 deletions awx/ui_next/src/components/CodeEditor/CodeEditor.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<FocusWrapper ref={wrapper} tabIndex={readOnly ? -1 : 0}>
<AceEditor
mode={aceModes[mode] || 'text'}
className={`pf-c-form-control ${className}`}
theme="github"
onChange={debounce(onChange, 250)}
value={value}
name={id || 'code-editor'}
editorProps={{ $blockScrolling: true }}
fontSize={16}
width="100%"
height={`${numRows * LINE_HEIGHT + PADDING}px`}
hasErrors={hasErrors}
setOptions={{
readOnly,
highlightActiveLine: !readOnly,
highlightGutterLine: !readOnly,
useWorker: false,
showPrintMargin: false,
}}
commands={[
{
name: 'escape',
bindKey: { win: 'Esc', mac: 'Esc' },
exec: () => {
wrapper.current.focus();
},
},
{
name: 'tab escape',
bindKey: { win: 'Shift-Tab', mac: 'Shift-Tab' },
exec: () => {
wrapper.current.focus();
},
},
]}
ref={editor}
/>
</FocusWrapper>
{!readOnly && (
<div
className="pf-c-form__helper-text keyboard-help-text"
aria-live="polite"
>
{i18n._(t`Press Enter to edit. Press ESC to stop editing.`)}
</div>
)}
</>
);
}
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);
44 changes: 44 additions & 0 deletions awx/ui_next/src/components/CodeEditor/CodeEditor.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
<CodeEditor value={'---\nfoo: bar'} onChange={onChange} mode="yaml" />
);
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(
<CodeEditor value="---" onChange={onChange} mode="yaml" />
);
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(
<CodeEditor value="---" onChange={onChange} mode="yaml" readOnly />
);
const aceEditor = wrapper.find('AceEditor');
expect(aceEditor.prop('setOptions').readOnly).toEqual(true);
expect(aceEditor.prop('value')).toEqual('---');
});
});

0 comments on commit 8ba9eef

Please sign in to comment.