mirror of
https://github.com/ansible/awx.git
synced 2026-02-16 02:30:01 -03:30
Add JSON/YAML components (#267)
Add CodeMirrorInput and VariablesField Add components for syntax highlighting, YAML/JSON toggle
This commit is contained in:
32
src/components/ButtonGroup.jsx
Normal file
32
src/components/ButtonGroup.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Group = styled.div`
|
||||
display: inline-flex;
|
||||
|
||||
& > .pf-c-button:not(:last-child) {
|
||||
&,
|
||||
&::after {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& > .pf-c-button:not(:first-child) {
|
||||
&,
|
||||
&::after {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function ButtonGroup ({ children }) {
|
||||
return (
|
||||
<Group>
|
||||
{children}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default ButtonGroup;
|
||||
66
src/components/CodeMirrorInput/CodeMirrorInput.jsx
Normal file
66
src/components/CodeMirrorInput/CodeMirrorInput.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { oneOf, bool, number, string, func } from 'prop-types';
|
||||
import { Controlled as ReactCodeMirror } from 'react-codemirror2';
|
||||
import styled from 'styled-components';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/yaml/yaml';
|
||||
import 'codemirror/mode/jinja2/jinja2';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
|
||||
const LINE_HEIGHT = 24;
|
||||
const PADDING = 12;
|
||||
|
||||
const CodeMirror = styled(ReactCodeMirror)`
|
||||
&& {
|
||||
height: initial;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& > .CodeMirror {
|
||||
height: ${props => (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);
|
||||
}`}
|
||||
`;
|
||||
|
||||
function CodeMirrorInput ({ value, onChange, mode, readOnly, hasErrors, rows }) {
|
||||
return (
|
||||
<CodeMirror
|
||||
className="pf-c-form-control"
|
||||
value={value}
|
||||
onBeforeChange={(editor, data, val) => onChange(val)}
|
||||
mode={mode}
|
||||
hasErrors={hasErrors}
|
||||
options={{
|
||||
smartIndent: false,
|
||||
lineNumbers: true,
|
||||
readOnly
|
||||
}}
|
||||
rows={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
CodeMirrorInput.propTypes = {
|
||||
value: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
|
||||
readOnly: bool,
|
||||
hasErrors: bool,
|
||||
rows: number,
|
||||
};
|
||||
CodeMirrorInput.defaultProps = {
|
||||
readOnly: false,
|
||||
rows: 6,
|
||||
hasErrors: false,
|
||||
};
|
||||
|
||||
export default CodeMirrorInput;
|
||||
39
src/components/CodeMirrorInput/CodeMirrorInput.test.jsx
Normal file
39
src/components/CodeMirrorInput/CodeMirrorInput.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
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(
|
||||
<CodeMirrorInput
|
||||
value="---\n"
|
||||
onChange={onChange}
|
||||
mode="yaml"
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<CodeMirrorInput
|
||||
value="---\n"
|
||||
onChange={onChange}
|
||||
mode="yaml"
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
const codemirror = wrapper.find('Controlled');
|
||||
expect(codemirror.prop('options').readOnly).toEqual(true);
|
||||
});
|
||||
});
|
||||
95
src/components/CodeMirrorInput/VariablesField.jsx
Normal file
95
src/components/CodeMirrorInput/VariablesField.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useState } from 'react';
|
||||
import { string, bool } from 'prop-types';
|
||||
import { Field } from 'formik';
|
||||
import { Button, Split, SplitItem } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
import { yamlToJson, jsonToYaml } from '../../util/yaml';
|
||||
|
||||
const YAML_MODE = 'yaml';
|
||||
const JSON_MODE = 'javascript';
|
||||
|
||||
const SmallButton = styled(Button)`
|
||||
padding: 3px 8px;
|
||||
font-size: var(--pf-global--FontSize--xs);
|
||||
`;
|
||||
|
||||
function VariablesField ({ id, name, label, readOnly }) {
|
||||
const [mode, setMode] = useState(YAML_MODE);
|
||||
|
||||
return (
|
||||
<Field
|
||||
name={name}
|
||||
render={({ field, form }) => (
|
||||
<div className="pf-c-form__group">
|
||||
<Split gutter="sm">
|
||||
<SplitItem>
|
||||
<label htmlFor={id} className="pf-c-form__label">{label}</label>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<ButtonGroup>
|
||||
<SmallButton
|
||||
onClick={() => {
|
||||
if (mode === YAML_MODE) { return; }
|
||||
try {
|
||||
form.setFieldValue(name, jsonToYaml(field.value));
|
||||
setMode(YAML_MODE);
|
||||
} catch (err) {
|
||||
form.setFieldError(name, err.message);
|
||||
}
|
||||
}}
|
||||
variant={mode === YAML_MODE ? 'primary' : 'secondary'}
|
||||
>
|
||||
YAML
|
||||
</SmallButton>
|
||||
<SmallButton
|
||||
onClick={() => {
|
||||
if (mode === JSON_MODE) { return; }
|
||||
try {
|
||||
form.setFieldValue(name, yamlToJson(field.value));
|
||||
setMode(JSON_MODE);
|
||||
} catch (err) {
|
||||
form.setFieldError(name, err.message);
|
||||
}
|
||||
}}
|
||||
variant={mode === JSON_MODE ? 'primary' : 'secondary'}
|
||||
>
|
||||
JSON
|
||||
</SmallButton>
|
||||
</ButtonGroup>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
<CodeMirrorInput
|
||||
mode={mode}
|
||||
readOnly={readOnly}
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue(name, value);
|
||||
}}
|
||||
hasErrors={!!form.errors[field.name]}
|
||||
/>
|
||||
{form.errors[field.name] ? (
|
||||
<div
|
||||
className="pf-c-form__helper-text pf-m-error"
|
||||
aria-live="polite"
|
||||
>
|
||||
{form.errors[field.name]}
|
||||
</div>
|
||||
) : null }
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
VariablesField.propTypes = {
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
label: string.isRequired,
|
||||
readOnly: bool,
|
||||
};
|
||||
VariablesField.defaultProps = {
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
export default VariablesField;
|
||||
111
src/components/CodeMirrorInput/VariablesField.test.jsx
Normal file
111
src/components/CodeMirrorInput/VariablesField.test.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Formik } from 'formik';
|
||||
import { sleep } from '../../../__tests__/testUtils';
|
||||
import VariablesField from './VariablesField';
|
||||
|
||||
describe('VariablesField', () => {
|
||||
beforeEach(() => {
|
||||
document.body.createTextRange = jest.fn();
|
||||
});
|
||||
|
||||
it('should render code mirror input', () => {
|
||||
const value = '---\n';
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
render={() => (
|
||||
<VariablesField
|
||||
id="the-field"
|
||||
name="variables"
|
||||
label="Variables"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
const codemirror = wrapper.find('Controlled');
|
||||
expect(codemirror.prop('value')).toEqual(value);
|
||||
});
|
||||
|
||||
it('should render yaml/json toggles', () => {
|
||||
const value = '---\n';
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
render={() => (
|
||||
<VariablesField
|
||||
id="the-field"
|
||||
name="variables"
|
||||
label="Variables"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
const buttons = wrapper.find('Button');
|
||||
expect(buttons).toHaveLength(2);
|
||||
expect(buttons.at(0).prop('variant')).toEqual('primary');
|
||||
expect(buttons.at(1).prop('variant')).toEqual('secondary');
|
||||
|
||||
buttons.at(1).simulate('click');
|
||||
wrapper.update(0);
|
||||
expect(wrapper.find('CodeMirrorInput').prop('mode')).toEqual('javascript');
|
||||
const buttons2 = wrapper.find('Button');
|
||||
expect(buttons2.at(0).prop('variant')).toEqual('secondary');
|
||||
expect(buttons2.at(1).prop('variant')).toEqual('primary');
|
||||
buttons2.at(0).simulate('click');
|
||||
wrapper.update(0);
|
||||
expect(wrapper.find('CodeMirrorInput').prop('mode')).toEqual('yaml');
|
||||
});
|
||||
|
||||
it('should set Formik error if yaml is invalid', () => {
|
||||
const value = '---\nfoo bar\n';
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
render={() => (
|
||||
<VariablesField
|
||||
id="the-field"
|
||||
name="variables"
|
||||
label="Variables"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
wrapper.find('Button').at(1).simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
const field = wrapper.find('CodeMirrorInput');
|
||||
expect(field.prop('hasErrors')).toEqual(true);
|
||||
expect(wrapper.find('.pf-m-error')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should submit value through Formik', async () => {
|
||||
const value = '---\nfoo: bar\n';
|
||||
const handleSubmit = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
onSubmit={handleSubmit}
|
||||
render={(formik) => (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<VariablesField
|
||||
id="the-field"
|
||||
name="variables"
|
||||
label="Variables"
|
||||
/>
|
||||
<button type="submit" id="submit">Submit</button>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
wrapper.find('CodeMirrorInput').prop('onChange')('---\nnewval: changed');
|
||||
wrapper.find('form').simulate('submit');
|
||||
await sleep(1);
|
||||
await sleep(1);
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalled();
|
||||
expect(handleSubmit.mock.calls[0][0]).toEqual({
|
||||
variables: '---\nnewval: changed'
|
||||
});
|
||||
});
|
||||
});
|
||||
4
src/components/CodeMirrorInput/index.js
Normal file
4
src/components/CodeMirrorInput/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
|
||||
export default CodeMirrorInput;
|
||||
export { default as VariablesField } from './VariablesField';
|
||||
17
src/util/yaml.js
Normal file
17
src/util/yaml.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export function yamlToJson (yamlString) {
|
||||
const value = yaml.safeLoad(yamlString);
|
||||
if (!value) { return '{}'; }
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('yaml is not in object format');
|
||||
}
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
export function jsonToYaml (jsonString) {
|
||||
if (jsonString.trim() === '') { return '---\n'; }
|
||||
const value = JSON.parse(jsonString);
|
||||
if (Object.entries(value).length === 0) { return '---\n'; }
|
||||
return yaml.safeDump(value);
|
||||
}
|
||||
Reference in New Issue
Block a user