mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Add feature to add Credential Type
Add feature to add Credential Type. See: https://github.com/ansible/awx/issues/7325
This commit is contained in:
parent
f5d38f57d4
commit
ad10f3581e
@ -179,7 +179,7 @@ describe('<CredentialAdd />', () => {
|
||||
expect(history.location.pathname).toBe('/credentials/13/details');
|
||||
});
|
||||
|
||||
test('handleCancel should return the user back to the inventories list', async () => {
|
||||
test('handleCancel should return the user back to the credentials list', async () => {
|
||||
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
||||
wrapper.find('Button[aria-label="Cancel"]').simulate('click');
|
||||
expect(history.location.pathname).toEqual('/credentials');
|
||||
|
||||
@ -1,11 +1,44 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import CredentialTypeForm from '../shared/CredentialTypeForm';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
import { CredentialTypesAPI } from '../../../api';
|
||||
import { parseVariableField } from '../../../util/yaml';
|
||||
|
||||
function CredentialTypeAdd() {
|
||||
const history = useHistory();
|
||||
const [submitError, setSubmitError] = useState(null);
|
||||
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
const { data: response } = await CredentialTypesAPI.create({
|
||||
...values,
|
||||
injectors: parseVariableField(values.injectors),
|
||||
inputs: parseVariableField(values.inputs),
|
||||
kind: 'cloud',
|
||||
});
|
||||
history.push(`/credential_types/${response.id}/details`);
|
||||
} catch (error) {
|
||||
setSubmitError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push(`/credential_types`);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<div>Credentials Type Add</div>
|
||||
<CardBody>
|
||||
<CredentialTypeForm
|
||||
onSubmit={handleSubmit}
|
||||
submitError={submitError}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import { CredentialTypesAPI } from '../../../api';
|
||||
import CredentialTypeAdd from './CredentialTypeAdd';
|
||||
|
||||
jest.mock('../../../api');
|
||||
|
||||
const credentialTypeData = {
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
kind: 'cloud',
|
||||
inputs: JSON.stringify({
|
||||
fields: [
|
||||
{
|
||||
id: 'username',
|
||||
type: 'string',
|
||||
label: 'Jenkins username',
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
type: 'string',
|
||||
label: 'Jenkins password',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
required: ['username', 'password'],
|
||||
}),
|
||||
injectors: JSON.stringify({
|
||||
extra_vars: {
|
||||
Jenkins_password: '{{ password }}',
|
||||
Jenkins_username: '{{ username }}',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
CredentialTypesAPI.create.mockResolvedValue({
|
||||
data: {
|
||||
id: 42,
|
||||
},
|
||||
});
|
||||
|
||||
describe('<CredentialTypeAdd/>', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/credential_types'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<CredentialTypeAdd />, {
|
||||
context: { router: { history } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('handleSubmit should call the api and redirect to details page', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('CredentialTypeForm').prop('onSubmit')(credentialTypeData);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(CredentialTypesAPI.create).toHaveBeenCalledWith({
|
||||
...credentialTypeData,
|
||||
inputs: JSON.parse(credentialTypeData.inputs),
|
||||
injectors: JSON.parse(credentialTypeData.injectors),
|
||||
});
|
||||
expect(history.location.pathname).toBe('/credential_types/42/details');
|
||||
});
|
||||
|
||||
test('handleCancel should return the user back to the credential types list', async () => {
|
||||
wrapper.find('Button[aria-label="Cancel"]').simulate('click');
|
||||
expect(history.location.pathname).toEqual('/credential_types');
|
||||
});
|
||||
|
||||
test('failed form submission should show an error message', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: { detail: 'An error occurred' },
|
||||
},
|
||||
};
|
||||
CredentialTypesAPI.create.mockImplementationOnce(() =>
|
||||
Promise.reject(error)
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('CredentialTypeForm').invoke('onSubmit')(credentialTypeData);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { Formik } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { Form } from '@patternfly/react-core';
|
||||
import { VariablesField } from '../../../components/CodeMirrorInput';
|
||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
import FormActionGroup from '../../../components/FormActionGroup';
|
||||
import { required } from '../../../util/validators';
|
||||
import {
|
||||
FormColumnLayout,
|
||||
FormFullWidthLayout,
|
||||
} from '../../../components/FormLayout';
|
||||
|
||||
function CredentialTypeFormFields({ i18n }) {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
id="credential-type-name"
|
||||
label={i18n._(t`Name`)}
|
||||
name="name"
|
||||
type="text"
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="credential-type-description"
|
||||
label={i18n._(t`Description`)}
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
<FormFullWidthLayout>
|
||||
<VariablesField
|
||||
tooltip={i18n._(
|
||||
t`Enter inputs using either JSON or YAML syntax. Refer to the Ansible Tower documentation for example syntax.`
|
||||
)}
|
||||
id="credential-type-inputs-configuration"
|
||||
name="inputs"
|
||||
label={i18n._(t`Input configuration`)}
|
||||
/>
|
||||
</FormFullWidthLayout>
|
||||
<FormFullWidthLayout>
|
||||
<VariablesField
|
||||
tooltip={i18n._(
|
||||
t`Enter injectors using either JSON or YAML syntax. Refer to the Ansible Tower documentation for example syntax.`
|
||||
)}
|
||||
id="credential-type-injectors-configuration"
|
||||
name="injectors"
|
||||
label={i18n._(t`Injector configuration`)}
|
||||
/>
|
||||
</FormFullWidthLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CredentialTypeForm({
|
||||
credentialType = {},
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitError,
|
||||
...rest
|
||||
}) {
|
||||
const initialValues = {
|
||||
name: credentialType.name || '',
|
||||
description: credentialType.description || '',
|
||||
inputs: credentialType.inputs || '---',
|
||||
injectors: credentialType.injectors || '---',
|
||||
};
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={values => onSubmit(values)}>
|
||||
{formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<CredentialTypeFormFields {...rest} />
|
||||
<FormSubmitError error={submitError} />
|
||||
<FormActionGroup
|
||||
onCancel={onCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</FormColumnLayout>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
CredentialTypeForm.propTypes = {
|
||||
credentialType: shape({}),
|
||||
onCancel: func.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
submitError: shape({}),
|
||||
};
|
||||
|
||||
CredentialTypeForm.defaultProps = {
|
||||
credentialType: {},
|
||||
submitError: null,
|
||||
};
|
||||
|
||||
export default withI18n()(CredentialTypeForm);
|
||||
@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
|
||||
import CredentialTypeForm from './CredentialTypeForm';
|
||||
|
||||
jest.mock('../../../api');
|
||||
|
||||
const credentialType = {
|
||||
id: 28,
|
||||
type: 'credential_type',
|
||||
url: '/api/v2/credential_types/28/',
|
||||
summary_fields: {
|
||||
created_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
modified_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
},
|
||||
created: '2020-06-18T14:48:47.869002Z',
|
||||
modified: '2020 - 06 - 18T14: 48: 47.869017Z',
|
||||
name: 'Jenkins Credential',
|
||||
description: 'Jenkins Credential',
|
||||
kind: 'cloud',
|
||||
namespace: null,
|
||||
managed_by_tower: false,
|
||||
inputs: JSON.stringify({
|
||||
fields: [
|
||||
{
|
||||
id: 'username',
|
||||
type: 'string',
|
||||
label: 'Jenkins username',
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
type: 'string',
|
||||
label: 'Jenkins password',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
required: ['username', 'password'],
|
||||
}),
|
||||
injectors: JSON.stringify({
|
||||
extra_vars: {
|
||||
Jenkins_password: '{{ password }}',
|
||||
Jenkins_username: '{{ username }}',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
describe('<CredentialTypeForm/>', () => {
|
||||
let wrapper;
|
||||
let onCancel;
|
||||
let onSubmit;
|
||||
|
||||
beforeEach(async () => {
|
||||
onCancel = jest.fn();
|
||||
onSubmit = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialTypeForm
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
credentialType={credentialType}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('Initially renders successfully', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should display form fields properly', () => {
|
||||
expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper.find('VariablesField[label="Input configuration"]').length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('VariablesField[label="Injector configuration"]').length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('should call onSubmit when form submitted', async () => {
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
});
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should update form values', () => {
|
||||
act(() => {
|
||||
wrapper.find('input#credential-type-name').simulate('change', {
|
||||
target: { value: 'Foo', name: 'name' },
|
||||
});
|
||||
wrapper.find('input#credential-type-description').simulate('change', {
|
||||
target: { value: 'New description', name: 'description' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#credential-type-name').prop('value')).toEqual(
|
||||
'Foo'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('input#credential-type-description').prop('value')
|
||||
).toEqual('New description');
|
||||
});
|
||||
|
||||
test('should call handleCancel when Cancel button is clicked', async () => {
|
||||
expect(onCancel).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||
expect(onCancel).toBeCalled();
|
||||
});
|
||||
});
|
||||
62
awx/ui_next/src/screens/CredentialType/shared/data.json
Normal file
62
awx/ui_next/src/screens/CredentialType/shared/data.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"id": 28,
|
||||
"type": "credential_type",
|
||||
"url": "/api/v2/credential_types/28/",
|
||||
"related": {
|
||||
"named_url": "/api/v2/credential_types/Jenkins Credential+cloud/",
|
||||
"created_by": "/api/v2/users/1/",
|
||||
"modified_by": "/api/v2/users/1/",
|
||||
"credentials": "/api/v2/credential_types/28/credentials/",
|
||||
"activity_stream": "/api/v2/credential_types/28/activity_stream/"
|
||||
},
|
||||
"summary_fields": {
|
||||
"created_by": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"modified_by": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"user_capabilities": {
|
||||
"edit": true,
|
||||
"delete": true
|
||||
}
|
||||
},
|
||||
"created": "2020-06-18T14:48:47.869002Z",
|
||||
"modified": "2020-06-18T14:48:47.869017Z",
|
||||
"name": "Jenkins Credential",
|
||||
"description": "Jenkins Credential",
|
||||
"kind": "cloud",
|
||||
"namespace": null,
|
||||
"managed_by_tower": false,
|
||||
"inputs": {
|
||||
"fields": [
|
||||
{
|
||||
"id": "username",
|
||||
"type": "string",
|
||||
"label": "Jenkins username"
|
||||
},
|
||||
{
|
||||
"id": "password",
|
||||
"type": "string",
|
||||
"label": "Jenkins password",
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"username",
|
||||
"password"
|
||||
]
|
||||
},
|
||||
"injectors": {
|
||||
"extra_vars": {
|
||||
"Jenkins_password": "{{ password }}",
|
||||
"Jenkins_username": "{{ username }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
awx/ui_next/src/screens/CredentialType/shared/index.js
Normal file
1
awx/ui_next/src/screens/CredentialType/shared/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CredentialTypeForm';
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
|
||||
import HostForm from '../../../components/HostForm';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
import { HostsAPI } from '../../../api';
|
||||
|
||||
function HostAdd() {
|
||||
|
||||
@ -35,3 +35,16 @@ export function isJson(jsonString) {
|
||||
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
export function parseVariableField(variableField) {
|
||||
if (variableField === '---' || variableField === '{}') {
|
||||
variableField = {};
|
||||
} else {
|
||||
if (!isJson(variableField)) {
|
||||
variableField = yamlToJson(variableField);
|
||||
}
|
||||
variableField = JSON.parse(variableField);
|
||||
}
|
||||
|
||||
return variableField;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { yamlToJson, jsonToYaml } from './yaml';
|
||||
import { yamlToJson, jsonToYaml, parseVariableField } from './yaml';
|
||||
|
||||
describe('yamlToJson', () => {
|
||||
test('should convert to json', () => {
|
||||
@ -68,3 +68,44 @@ two: two
|
||||
expect(() => jsonToYaml('bad data')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseVariableField', () => {
|
||||
const injector = `
|
||||
extra_vars:
|
||||
password: '{{ password }}'
|
||||
username: '{{ username }}'
|
||||
`;
|
||||
|
||||
const input = `{
|
||||
"fields": [
|
||||
{
|
||||
"id": "username",
|
||||
"type": "string",
|
||||
"label": "Username"
|
||||
},
|
||||
{
|
||||
"id": "password",
|
||||
"type": "string",
|
||||
"label": "Password",
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
}`;
|
||||
test('should convert empty yaml to an object', () => {
|
||||
expect(parseVariableField('---')).toEqual({});
|
||||
});
|
||||
|
||||
test('should convert empty json to an object', () => {
|
||||
expect(parseVariableField('{}')).toEqual({});
|
||||
});
|
||||
|
||||
test('should convert yaml string to json object', () => {
|
||||
expect(parseVariableField(injector)).toEqual(
|
||||
JSON.parse(yamlToJson(injector))
|
||||
);
|
||||
});
|
||||
|
||||
test('should convert json string to json object', () => {
|
||||
expect(parseVariableField(input)).toEqual(JSON.parse(input));
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user