Merge pull request #7426 from nixocio/ui_issue_7325

Add feature to add Credential Type

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-06-29 18:54:27 +00:00 committed by GitHub
commit 879ab50a12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 487 additions and 5 deletions

View File

@ -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');

View File

@ -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>
);

View File

@ -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);
});
});

View File

@ -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);

View File

@ -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();
});
});

View 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 }}"
}
}
}

View File

@ -0,0 +1 @@
export { default } from './CredentialTypeForm';

View File

@ -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() {

View File

@ -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;
}

View File

@ -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));
});
});