mirror of
https://github.com/ansible/awx.git
synced 2026-01-23 15:38:06 -03:30
Fixes missing credential types and makes credential type drop down a typeahead component
This commit is contained in:
parent
2ef08b1d13
commit
4a2a6949a8
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
@ -14,9 +14,6 @@ import CredentialForm from '../shared/CredentialForm';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
|
||||
function CredentialAdd({ me }) {
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [credentialTypes, setCredentialTypes] = useState(null);
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
@ -85,34 +82,38 @@ function CredentialAdd({ me }) {
|
||||
history.push(`/credentials/${credentialId}/details`);
|
||||
}
|
||||
}, [credentialId, history]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const { isLoading, error, request: loadData, result } = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await CredentialTypesAPI.read({ page_size: 200 });
|
||||
const credTypes = data.results;
|
||||
if (data.next && data.next.includes('page=2')) {
|
||||
const {
|
||||
data: { results: loadedCredentialTypes },
|
||||
} = await CredentialTypesAPI.read();
|
||||
setCredentialTypes(
|
||||
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {})
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
data: { results },
|
||||
} = await CredentialTypesAPI.read({
|
||||
page_size: 200,
|
||||
page: 2,
|
||||
});
|
||||
credTypes.concat(results);
|
||||
}
|
||||
};
|
||||
|
||||
const creds = credTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {});
|
||||
return creds;
|
||||
}, []),
|
||||
{}
|
||||
);
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
}, [loadData]);
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push('/credentials');
|
||||
};
|
||||
|
||||
const handleSubmit = async values => {
|
||||
await submitRequest(values, credentialTypes);
|
||||
await submitRequest(values, result);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
@ -126,7 +127,7 @@ function CredentialAdd({ me }) {
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
if (isLoading && !result) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
@ -144,7 +145,7 @@ function CredentialAdd({ me }) {
|
||||
<CredentialForm
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
credentialTypes={credentialTypes}
|
||||
credentialTypes={result}
|
||||
submitError={submitError}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { object } from 'prop-types';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
import {
|
||||
@ -13,11 +13,8 @@ import CredentialForm from '../shared/CredentialForm';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
|
||||
function CredentialEdit({ credential, me }) {
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [credentialTypes, setCredentialTypes] = useState(null);
|
||||
const [inputSources, setInputSources] = useState({});
|
||||
const history = useHistory();
|
||||
const { id: credId } = useParams();
|
||||
|
||||
const { error: submitError, request: submitRequest, result } = useRequest(
|
||||
useCallback(
|
||||
@ -55,7 +52,7 @@ function CredentialEdit({ credential, me }) {
|
||||
input_field_name: fieldName,
|
||||
metadata: fieldValue.inputs,
|
||||
source_credential: fieldValue.credential.id,
|
||||
target_credential: credential.id,
|
||||
target_credential: credId,
|
||||
});
|
||||
}
|
||||
if (fieldValue.touched) {
|
||||
@ -88,7 +85,7 @@ function CredentialEdit({ credential, me }) {
|
||||
modifiedData.user = me.id;
|
||||
}
|
||||
const [{ data }] = await Promise.all([
|
||||
CredentialsAPI.update(credential.id, modifiedData),
|
||||
CredentialsAPI.update(credId, modifiedData),
|
||||
...destroyInputSources(),
|
||||
]);
|
||||
|
||||
@ -96,7 +93,7 @@ function CredentialEdit({ credential, me }) {
|
||||
|
||||
return data;
|
||||
},
|
||||
[me, credential.id]
|
||||
[me, credId]
|
||||
)
|
||||
);
|
||||
|
||||
@ -105,56 +102,63 @@ function CredentialEdit({ credential, me }) {
|
||||
history.push(`/credentials/${result.id}/details`);
|
||||
}
|
||||
}, [result, history]);
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
request: loadData,
|
||||
result: { credentialTypes, loadedInputSources },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [
|
||||
{ data },
|
||||
{
|
||||
data: { results },
|
||||
},
|
||||
] = await Promise.all([
|
||||
CredentialTypesAPI.read({ page_size: 200 }),
|
||||
CredentialsAPI.readInputSources(credId, { page_size: 200 }),
|
||||
]);
|
||||
const credTypes = data.results;
|
||||
if (data.next && data.next.includes('page=2')) {
|
||||
const {
|
||||
data: { results: additionalCredTypes },
|
||||
} = await CredentialTypesAPI.read({
|
||||
page_size: 200,
|
||||
page: 2,
|
||||
});
|
||||
credTypes.concat([...additionalCredTypes]);
|
||||
}
|
||||
const creds = credTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {});
|
||||
const inputSources = results.reduce((inputSourcesMap, inputSource) => {
|
||||
inputSourcesMap[inputSource.input_field_name] = inputSource;
|
||||
return inputSourcesMap;
|
||||
}, {});
|
||||
return { credentialTypes: creds, loadedInputSources: inputSources };
|
||||
}, [credId]),
|
||||
{ credentialTypes: {}, loadedInputSources: {} }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: { results: loadedCredentialTypes },
|
||||
},
|
||||
{
|
||||
data: { results: loadedInputSources },
|
||||
},
|
||||
] = await Promise.all([
|
||||
CredentialTypesAPI.read(),
|
||||
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
|
||||
]);
|
||||
setCredentialTypes(
|
||||
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
|
||||
credentialTypesMap[credentialType.id] = credentialType;
|
||||
return credentialTypesMap;
|
||||
}, {})
|
||||
);
|
||||
setInputSources(
|
||||
loadedInputSources.reduce((inputSourcesMap, inputSource) => {
|
||||
inputSourcesMap[inputSource.input_field_name] = inputSource;
|
||||
return inputSourcesMap;
|
||||
}, {})
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, [credential.id]);
|
||||
}, [loadData]);
|
||||
|
||||
const handleCancel = () => {
|
||||
const url = `/credentials/${credential.id}/details`;
|
||||
const url = `/credentials/${credId}/details`;
|
||||
history.push(`${url}`);
|
||||
};
|
||||
|
||||
const handleSubmit = async values => {
|
||||
await submitRequest(values, credentialTypes, inputSources);
|
||||
await submitRequest(values, credentialTypes, loadedInputSources);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && !credentialTypes) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
@ -165,7 +169,7 @@ function CredentialEdit({ credential, me }) {
|
||||
onSubmit={handleSubmit}
|
||||
credential={credential}
|
||||
credentialTypes={credentialTypes}
|
||||
inputSources={inputSources}
|
||||
inputSources={loadedInputSources}
|
||||
submitError={submitError}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
@ -14,6 +14,12 @@ import {
|
||||
import CredentialEdit from './CredentialEdit';
|
||||
|
||||
jest.mock('../../../api');
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
id: 3,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockCredential = {
|
||||
id: 3,
|
||||
|
||||
@ -3,26 +3,28 @@ import { Formik, useField, useFormikContext } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { arrayOf, func, object, shape } from 'prop-types';
|
||||
import { ActionGroup, Button, Form, FormGroup } from '@patternfly/react-core';
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
import {
|
||||
FormColumnLayout,
|
||||
FormFullWidthLayout,
|
||||
} from '../../../components/FormLayout';
|
||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||
import { required } from '../../../util/validators';
|
||||
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
||||
import TypeInputsSubForm from './TypeInputsSubForm';
|
||||
import ExternalTestModal from './ExternalTestModal';
|
||||
|
||||
function CredentialFormFields({
|
||||
i18n,
|
||||
credentialTypes,
|
||||
formik,
|
||||
initialValues,
|
||||
}) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
function CredentialFormFields({ i18n, credentialTypes }) {
|
||||
const { setFieldValue, initialValues, setFieldTouched } = useFormikContext();
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
|
||||
name: 'credential_type',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
@ -30,7 +32,7 @@ function CredentialFormFields({
|
||||
|
||||
const isGalaxyCredential =
|
||||
!!credTypeField.value &&
|
||||
credentialTypes[credTypeField.value].kind === 'galaxy';
|
||||
credentialTypes[credTypeField.value]?.kind === 'galaxy';
|
||||
|
||||
const [orgField, orgMeta, orgHelpers] = useField({
|
||||
name: 'organization',
|
||||
@ -52,16 +54,14 @@ function CredentialFormFields({
|
||||
})
|
||||
.sort((a, b) => (a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1));
|
||||
|
||||
const resetSubFormFields = (newCredentialType, form) => {
|
||||
const resetSubFormFields = newCredentialType => {
|
||||
const fields = credentialTypes[newCredentialType].inputs.fields || [];
|
||||
fields.forEach(
|
||||
({ ask_at_runtime, type, id, choices, default: defaultValue }) => {
|
||||
if (
|
||||
parseInt(newCredentialType, 10) === form.initialValues.credential_type
|
||||
) {
|
||||
form.setFieldValue(`inputs.${id}`, initialValues.inputs[id]);
|
||||
if (parseInt(newCredentialType, 10) === initialValues.credential_type) {
|
||||
setFieldValue(`inputs.${id}`, initialValues.inputs[id]);
|
||||
if (ask_at_runtime) {
|
||||
form.setFieldValue(
|
||||
setFieldValue(
|
||||
`passwordPrompts.${id}`,
|
||||
initialValues.passwordPrompts[id]
|
||||
);
|
||||
@ -69,24 +69,24 @@ function CredentialFormFields({
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
form.setFieldValue(`inputs.${id}`, defaultValue || '');
|
||||
setFieldValue(`inputs.${id}`, defaultValue || '');
|
||||
break;
|
||||
case 'boolean':
|
||||
form.setFieldValue(`inputs.${id}`, defaultValue || false);
|
||||
setFieldValue(`inputs.${id}`, defaultValue || false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (choices) {
|
||||
form.setFieldValue(`inputs.${id}`, defaultValue);
|
||||
setFieldValue(`inputs.${id}`, defaultValue);
|
||||
}
|
||||
|
||||
if (ask_at_runtime) {
|
||||
form.setFieldValue(`passwordPrompts.${id}`, false);
|
||||
setFieldValue(`passwordPrompts.${id}`, false);
|
||||
}
|
||||
}
|
||||
form.setFieldTouched(`inputs.${id}`, false);
|
||||
setFieldTouched(`inputs.${id}`, false);
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -133,23 +133,27 @@ function CredentialFormFields({
|
||||
}
|
||||
label={i18n._(t`Credential Type`)}
|
||||
>
|
||||
<AnsibleSelect
|
||||
{...credTypeField}
|
||||
<Select
|
||||
aria-label={i18n._(t`Credential Type`)}
|
||||
isOpen={isSelectOpen}
|
||||
variant={SelectVariant.typeahead}
|
||||
id="credential-type"
|
||||
data={[
|
||||
{
|
||||
value: '',
|
||||
key: '',
|
||||
label: i18n._(t`Choose a Credential Type`),
|
||||
isDisabled: true,
|
||||
},
|
||||
...credentialTypeOptions,
|
||||
]}
|
||||
onChange={(event, value) => {
|
||||
onToggle={setIsSelectOpen}
|
||||
onSelect={(event, value) => {
|
||||
credTypeHelpers.setValue(value);
|
||||
resetSubFormFields(value, formik);
|
||||
resetSubFormFields(value);
|
||||
}}
|
||||
/>
|
||||
selections={credTypeField.value}
|
||||
placeholder={i18n._(t`Select a credential Type`)}
|
||||
isCreatable={false}
|
||||
maxHeight="300px"
|
||||
>
|
||||
{credentialTypeOptions.map(credType => (
|
||||
<SelectOption key={credType.value} value={credType.value}>
|
||||
{credType.label}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
{credTypeField.value !== undefined &&
|
||||
credTypeField.value !== '' &&
|
||||
@ -177,7 +181,7 @@ function CredentialForm({
|
||||
name: credential.name || '',
|
||||
description: credential.description || '',
|
||||
organization: credential?.summary_fields?.organization || null,
|
||||
credential_type: credential.credential_type || '',
|
||||
credential_type: credential?.credential_type || '',
|
||||
inputs: {},
|
||||
passwordPrompts: {},
|
||||
};
|
||||
@ -235,8 +239,6 @@ function CredentialForm({
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<CredentialFormFields
|
||||
formik={formik}
|
||||
initialValues={initialValues}
|
||||
credentialTypes={credentialTypes}
|
||||
i18n={i18n}
|
||||
{...rest}
|
||||
|
||||
@ -137,15 +137,28 @@ describe('<CredentialForm />', () => {
|
||||
test('should display cred type subform when scm type select has a value', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 1);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
machineFieldExpects();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 2);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 2);
|
||||
});
|
||||
wrapper.update();
|
||||
sourceFieldExpects();
|
||||
@ -154,8 +167,14 @@ describe('<CredentialForm />', () => {
|
||||
test('should update expected fields when gce service account json file uploaded', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 10);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 10);
|
||||
});
|
||||
wrapper.update();
|
||||
gceFieldExpects();
|
||||
@ -215,8 +234,14 @@ describe('<CredentialForm />', () => {
|
||||
test('should show error when error thrown parsing JSON', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 10);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 10);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('#credential-gce-file-helper').text()).toBe(
|
||||
@ -246,8 +271,14 @@ describe('<CredentialForm />', () => {
|
||||
test('should show Test button when external credential type is selected', async () => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('AnsibleSelect[id="credential-type"]')
|
||||
.invoke('onChange')(null, 21);
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onToggle')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find('Select[aria-label="Credential Type"]')
|
||||
.invoke('onSelect')(null, 21);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[children="Test"]').length).toBe(1);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user