Merge pull request #7489 from nixocio/ui_issue_7326

Add edit credential types

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-07-08 20:53:09 +00:00 committed by GitHub
commit d0ac028265
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 23 deletions

View File

@ -22,6 +22,8 @@ function FormSubmitError({ error }) {
Object.values(error.response.data).forEach(value => {
if (Array.isArray(value)) {
messages = messages.concat(value);
} else {
messages.push(value);
}
});
setErrorMessage(messages.length > 0 ? messages : null);

View File

@ -65,11 +65,6 @@ function CredentialType({ i18n, setBreadcrumb }) {
},
];
let cardHeader = <RoutedTabs tabsArray={tabsArray} />;
if (pathname.endsWith('edit')) {
cardHeader = null;
}
if (!isLoading && contentError) {
return (
<PageSection>
@ -89,6 +84,11 @@ function CredentialType({ i18n, setBreadcrumb }) {
);
}
let cardHeader = <RoutedTabs tabsArray={tabsArray} />;
if (pathname.endsWith('edit')) {
cardHeader = null;
}
return (
<PageSection>
<Card>
@ -104,7 +104,7 @@ function CredentialType({ i18n, setBreadcrumb }) {
{credentialType && (
<>
<Route path="/credential_types/:id/edit">
<CredentialTypeEdit />
<CredentialTypeEdit credentialType={credentialType} />
</Route>
<Route path="/credential_types/:id/details">
<CredentialTypeDetails credentialType={credentialType} />

View File

@ -70,7 +70,7 @@ function CredentialTypeDetails({ credentialType, i18n }) {
<Button
aria-label={i18n._(t`edit`)}
component={Link}
to={`credential_types/${id}/edit`}
to={`/credential_types/${id}/edit`}
>
{i18n._(t`Edit`)}
</Button>

View File

@ -1,11 +1,41 @@
import React from 'react';
import { Card, PageSection } from '@patternfly/react-core';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
function CredentialTypeEdit() {
import { CardBody } from '../../../components/Card';
import { CredentialTypesAPI } from '../../../api';
import CredentialTypeForm from '../shared/CredentialTypeForm';
import { parseVariableField } from '../../../util/yaml';
function CredentialTypeEdit({ credentialType }) {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
const detailsUrl = `/credential_types/${credentialType.id}/details`;
const handleSubmit = async values => {
try {
await CredentialTypesAPI.update(credentialType.id, {
...values,
injectors: parseVariableField(values.injectors),
inputs: parseVariableField(values.inputs),
});
history.push(detailsUrl);
} catch (error) {
setSubmitError(error);
}
};
const handleCancel = () => {
history.push(detailsUrl);
};
return (
<PageSection>
<Card>Credential Type Edit</Card>
</PageSection>
<CardBody>
<CredentialTypeForm
credentialType={credentialType}
onSubmit={handleSubmit}
submitError={submitError}
onCancel={handleCancel}
/>
</CardBody>
);
}

View File

@ -0,0 +1,140 @@
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 CredentialTypeEdit from './CredentialTypeEdit';
jest.mock('../../../api');
const credentialTypeData = {
id: 42,
name: 'Foo',
description: 'New credential',
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 }}',
},
}),
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-25T16:52:36.127008Z',
modified: '2020-06-25T16:52:36.127022Z',
};
const updateCredentialTypeData = {
name: 'Bar',
description: 'Updated new Credential Type',
injectors: credentialTypeData.injectors,
inputs: credentialTypeData.inputs,
};
describe('<CredentialTypeEdit>', () => {
let wrapper;
let history;
beforeAll(async () => {
history = createMemoryHistory();
await act(async () => {
wrapper = mountWithContexts(
<CredentialTypeEdit credentialType={credentialTypeData} />,
{
context: { router: { history } },
}
);
});
});
afterAll(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('handleSubmit should call the api and redirect to details page', async () => {
await act(async () => {
wrapper.find('CredentialTypeForm').invoke('onSubmit')(
updateCredentialTypeData
);
wrapper.update();
expect(CredentialTypesAPI.update).toHaveBeenCalledWith(42, {
...updateCredentialTypeData,
injectors: JSON.parse(credentialTypeData.injectors),
inputs: JSON.parse(credentialTypeData.inputs),
});
});
});
test('should navigate to credential types detail when cancel is clicked', async () => {
await act(async () => {
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
});
expect(history.location.pathname).toEqual('/credential_types/42/details');
});
test('should navigate to credential type detail after successful submission', async () => {
await act(async () => {
wrapper.find('CredentialTypeForm').invoke('onSubmit')({
...updateCredentialTypeData,
injectors: JSON.parse(credentialTypeData.injectors),
inputs: JSON.parse(credentialTypeData.inputs),
});
});
wrapper.update();
expect(wrapper.find('FormSubmitError').length).toBe(0);
expect(history.location.pathname).toEqual('/credential_types/42/details');
});
test('failed form submission should show an error message', async () => {
const error = {
response: {
data: { detail: 'An error occurred' },
},
};
CredentialTypesAPI.update.mockImplementationOnce(() =>
Promise.reject(error)
);
await act(async () => {
wrapper.find('CredentialTypeForm').invoke('onSubmit')(
updateCredentialTypeData
);
});
wrapper.update();
expect(wrapper.find('FormSubmitError').length).toBe(1);
});
});

View File

@ -131,7 +131,7 @@ function CredentialTypeList({ i18n }) {
)}
renderItem={credentialType => (
<CredentialTypeListItem
key={credentialTypes.id}
key={credentialType.id}
value={credentialType.name}
credentialType={credentialType}
detailUrl={`${match.url}/${credentialType.id}/details`}

View File

@ -14,6 +14,8 @@ import {
FormFullWidthLayout,
} from '../../../components/FormLayout';
import { jsonToYaml } from '../../../util/yaml';
function CredentialTypeFormFields({ i18n }) {
return (
<>
@ -65,8 +67,12 @@ function CredentialTypeForm({
const initialValues = {
name: credentialType.name || '',
description: credentialType.description || '',
inputs: credentialType.inputs || '---',
injectors: credentialType.injectors || '---',
inputs: credentialType.inputs
? jsonToYaml(JSON.stringify(credentialType.inputs))
: '---',
injectors: credentialType.injectors
? jsonToYaml(JSON.stringify(credentialType.injectors))
: '---',
};
return (
<Formik initialValues={initialValues} onSubmit={values => onSubmit(values)}>
@ -74,7 +80,7 @@ function CredentialTypeForm({
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<CredentialTypeFormFields {...rest} />
<FormSubmitError error={submitError} />
{submitError && <FormSubmitError error={submitError} />}
<FormActionGroup
onCancel={onCancel}
onSubmit={formik.handleSubmit}

View File

@ -38,13 +38,12 @@ export function isJson(jsonString) {
export function parseVariableField(variableField) {
if (variableField === '---' || variableField === '{}') {
variableField = {};
} else {
if (!isJson(variableField)) {
variableField = yamlToJson(variableField);
}
variableField = JSON.parse(variableField);
return {};
}
if (!isJson(variableField)) {
variableField = yamlToJson(variableField);
}
variableField = JSON.parse(variableField);
return variableField;
}