mirror of
https://github.com/ansible/awx.git
synced 2026-03-18 17:37:30 -02:30
Dynamically render credential subform fields based on options responses for each credential type
This commit is contained in:
@@ -7,16 +7,11 @@ import { CheckboxField, FieldTooltip } from '../FormField';
|
|||||||
|
|
||||||
const FieldHeader = styled.div`
|
const FieldHeader = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
padding-bottom: var(--pf-c-form__label--PaddingBottom);
|
|
||||||
|
|
||||||
label {
|
|
||||||
--pf-c-form__label--PaddingBottom: 0px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCheckboxField = styled(CheckboxField)`
|
const StyledCheckboxField = styled(CheckboxField)`
|
||||||
--pf-c-check__label--FontSize: var(--pf-c-form__label--FontSize);
|
--pf-c-check__label--FontSize: var(--pf-c-form__label--FontSize);
|
||||||
|
margin-left: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function FieldWithPrompt({
|
function FieldWithPrompt({
|
||||||
|
|||||||
@@ -25,17 +25,32 @@ function CredentialAdd({ me }) {
|
|||||||
result: credentialId,
|
result: credentialId,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(
|
useCallback(
|
||||||
async values => {
|
async (values, credentialTypesMap) => {
|
||||||
const { inputs, organization, ...remainingValues } = values;
|
const {
|
||||||
|
inputs: { fields: possibleFields },
|
||||||
|
} = credentialTypesMap[values.credential_type];
|
||||||
|
|
||||||
|
const {
|
||||||
|
inputs,
|
||||||
|
organization,
|
||||||
|
passwordPrompts,
|
||||||
|
...remainingValues
|
||||||
|
} = values;
|
||||||
|
|
||||||
const nonPluginInputs = {};
|
const nonPluginInputs = {};
|
||||||
const pluginInputs = {};
|
const pluginInputs = {};
|
||||||
Object.entries(inputs).forEach(([key, value]) => {
|
|
||||||
if (value.credential && value.inputs) {
|
possibleFields.forEach(field => {
|
||||||
pluginInputs[key] = value;
|
const input = inputs[field.id];
|
||||||
|
if (input.credential && input.inputs) {
|
||||||
|
pluginInputs[field.id] = input;
|
||||||
|
} else if (passwordPrompts[field.id]) {
|
||||||
|
nonPluginInputs[field.id] = 'ASK';
|
||||||
} else {
|
} else {
|
||||||
nonPluginInputs[key] = value;
|
nonPluginInputs[field.id] = input;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { id: newCredentialId },
|
data: { id: newCredentialId },
|
||||||
} = await CredentialsAPI.create({
|
} = await CredentialsAPI.create({
|
||||||
@@ -44,18 +59,17 @@ function CredentialAdd({ me }) {
|
|||||||
inputs: nonPluginInputs,
|
inputs: nonPluginInputs,
|
||||||
...remainingValues,
|
...remainingValues,
|
||||||
});
|
});
|
||||||
const inputSourceRequests = [];
|
|
||||||
Object.entries(pluginInputs).forEach(([key, value]) => {
|
await Promise.all(
|
||||||
inputSourceRequests.push(
|
Object.entries(pluginInputs).map(([key, value]) =>
|
||||||
CredentialInputSourcesAPI.create({
|
CredentialInputSourcesAPI.create({
|
||||||
input_field_name: key,
|
input_field_name: key,
|
||||||
metadata: value.inputs,
|
metadata: value.inputs,
|
||||||
source_credential: value.credential.id,
|
source_credential: value.credential.id,
|
||||||
target_credential: newCredentialId,
|
target_credential: newCredentialId,
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
});
|
);
|
||||||
await Promise.all(inputSourceRequests);
|
|
||||||
|
|
||||||
return newCredentialId;
|
return newCredentialId;
|
||||||
},
|
},
|
||||||
@@ -74,10 +88,13 @@ function CredentialAdd({ me }) {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { results: loadedCredentialTypes },
|
data: { results: loadedCredentialTypes },
|
||||||
} = await CredentialTypesAPI.read({
|
} = await CredentialTypesAPI.read();
|
||||||
or__namespace: ['gce', 'scm', 'ssh'],
|
setCredentialTypes(
|
||||||
});
|
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
|
||||||
setCredentialTypes(loadedCredentialTypes);
|
credentialTypesMap[credentialType.id] = credentialType;
|
||||||
|
return credentialTypesMap;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -92,7 +109,7 @@ function CredentialAdd({ me }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
await submitRequest(values);
|
await submitRequest(values, credentialTypes);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { sleep } from '../../../../testUtils/testUtils';
|
|
||||||
|
|
||||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
||||||
import CredentialAdd from './CredentialAdd';
|
import CredentialAdd from './CredentialAdd';
|
||||||
@@ -175,23 +174,34 @@ describe('<CredentialAdd />', () => {
|
|||||||
});
|
});
|
||||||
test('handleSubmit should call the api and redirect to details page', async () => {
|
test('handleSubmit should call the api and redirect to details page', async () => {
|
||||||
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
||||||
|
await act(async () => {
|
||||||
wrapper.find('CredentialForm').prop('onSubmit')({
|
wrapper.find('CredentialForm').prop('onSubmit')({
|
||||||
user: 1,
|
user: 1,
|
||||||
organization: null,
|
organization: null,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
credential_type: '2',
|
credential_type: '2',
|
||||||
inputs: {},
|
inputs: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
ssh_key_data: '',
|
||||||
|
ssh_key_unlock: '',
|
||||||
|
},
|
||||||
|
passwordPrompts: {},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await sleep(1);
|
|
||||||
expect(CredentialsAPI.create).toHaveBeenCalledWith({
|
expect(CredentialsAPI.create).toHaveBeenCalledWith({
|
||||||
user: 1,
|
user: 1,
|
||||||
organization: null,
|
organization: null,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
credential_type: '2',
|
credential_type: '2',
|
||||||
inputs: {},
|
inputs: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
ssh_key_data: '',
|
||||||
|
ssh_key_unlock: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(history.location.pathname).toBe('/credentials/13/details');
|
expect(history.location.pathname).toBe('/credentials/13/details');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useState, useEffect } from 'react';
|
import React, { useCallback, useState, useEffect } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { object } from 'prop-types';
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
import {
|
import {
|
||||||
CredentialsAPI,
|
CredentialsAPI,
|
||||||
@@ -22,8 +21,33 @@ function CredentialEdit({ credential, me }) {
|
|||||||
|
|
||||||
const { error: submitError, request: submitRequest, result } = useRequest(
|
const { error: submitError, request: submitRequest, result } = useRequest(
|
||||||
useCallback(
|
useCallback(
|
||||||
async (values, inputSourceMap) => {
|
async (values, credentialTypesMap, inputSourceMap) => {
|
||||||
const createAndUpdateInputSources = pluginInputs =>
|
const {
|
||||||
|
inputs: { fields: possibleFields },
|
||||||
|
} = credentialTypesMap[values.credential_type];
|
||||||
|
|
||||||
|
const {
|
||||||
|
inputs,
|
||||||
|
organization,
|
||||||
|
passwordPrompts,
|
||||||
|
...remainingValues
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
const nonPluginInputs = {};
|
||||||
|
const pluginInputs = {};
|
||||||
|
|
||||||
|
possibleFields.forEach(field => {
|
||||||
|
const input = inputs[field.id];
|
||||||
|
if (input.credential && input.inputs) {
|
||||||
|
pluginInputs[field.id] = input;
|
||||||
|
} else if (passwordPrompts[field.id]) {
|
||||||
|
nonPluginInputs[field.id] = 'ASK';
|
||||||
|
} else {
|
||||||
|
nonPluginInputs[field.id] = input;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createAndUpdateInputSources = () =>
|
||||||
Object.entries(pluginInputs).map(([fieldName, fieldValue]) => {
|
Object.entries(pluginInputs).map(([fieldName, fieldValue]) => {
|
||||||
if (!inputSourceMap[fieldName]) {
|
if (!inputSourceMap[fieldName]) {
|
||||||
return CredentialInputSourcesAPI.create({
|
return CredentialInputSourcesAPI.create({
|
||||||
@@ -46,27 +70,15 @@ function CredentialEdit({ credential, me }) {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const destroyInputSources = inputs => {
|
const destroyInputSources = () =>
|
||||||
const destroyRequests = [];
|
Object.values(inputSourceMap).map(inputSource => {
|
||||||
Object.values(inputSourceMap).forEach(inputSource => {
|
|
||||||
const { id, input_field_name } = inputSource;
|
const { id, input_field_name } = inputSource;
|
||||||
if (!inputs[input_field_name]?.credential) {
|
if (!inputs[input_field_name]?.credential) {
|
||||||
destroyRequests.push(CredentialInputSourcesAPI.destroy(id));
|
return CredentialInputSourcesAPI.destroy(id);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
return destroyRequests;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { inputs, organization, ...remainingValues } = values;
|
|
||||||
const nonPluginInputs = {};
|
|
||||||
const pluginInputs = {};
|
|
||||||
Object.entries(inputs).forEach(([key, value]) => {
|
|
||||||
if (value.credential && value.inputs) {
|
|
||||||
pluginInputs[key] = value;
|
|
||||||
} else {
|
|
||||||
nonPluginInputs[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const [{ data }] = await Promise.all([
|
const [{ data }] = await Promise.all([
|
||||||
CredentialsAPI.update(credential.id, {
|
CredentialsAPI.update(credential.id, {
|
||||||
user: (me && me.id) || null,
|
user: (me && me.id) || null,
|
||||||
@@ -74,12 +86,14 @@ function CredentialEdit({ credential, me }) {
|
|||||||
inputs: nonPluginInputs,
|
inputs: nonPluginInputs,
|
||||||
...remainingValues,
|
...remainingValues,
|
||||||
}),
|
}),
|
||||||
...destroyInputSources(inputs),
|
...destroyInputSources(),
|
||||||
]);
|
]);
|
||||||
await Promise.all(createAndUpdateInputSources(pluginInputs));
|
|
||||||
|
await Promise.all(createAndUpdateInputSources());
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
[credential.id, me]
|
[me, credential.id]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,12 +114,15 @@ function CredentialEdit({ credential, me }) {
|
|||||||
data: { results: loadedInputSources },
|
data: { results: loadedInputSources },
|
||||||
},
|
},
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
CredentialTypesAPI.read({
|
CredentialTypesAPI.read(),
|
||||||
or__namespace: ['gce', 'scm', 'ssh'],
|
|
||||||
}),
|
|
||||||
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
|
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
|
||||||
]);
|
]);
|
||||||
setCredentialTypes(loadedCredentialTypes);
|
setCredentialTypes(
|
||||||
|
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
|
||||||
|
credentialTypesMap[credentialType.id] = credentialType;
|
||||||
|
return credentialTypesMap;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
setInputSources(
|
setInputSources(
|
||||||
loadedInputSources.reduce((inputSourcesMap, inputSource) => {
|
loadedInputSources.reduce((inputSourcesMap, inputSource) => {
|
||||||
inputSourcesMap[inputSource.input_field_name] = inputSource;
|
inputSourcesMap[inputSource.input_field_name] = inputSource;
|
||||||
@@ -127,7 +144,7 @@ function CredentialEdit({ credential, me }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
await submitRequest(values, inputSources);
|
await submitRequest(values, credentialTypes, inputSources);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { sleep } from '../../../../testUtils/testUtils';
|
|
||||||
|
|
||||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
||||||
import CredentialEdit from './CredentialEdit';
|
import CredentialEdit from './CredentialEdit';
|
||||||
@@ -279,23 +278,34 @@ describe('<CredentialEdit />', () => {
|
|||||||
|
|
||||||
test('handleSubmit should post to the api', async () => {
|
test('handleSubmit should post to the api', async () => {
|
||||||
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
||||||
|
await act(async () => {
|
||||||
wrapper.find('CredentialForm').prop('onSubmit')({
|
wrapper.find('CredentialForm').prop('onSubmit')({
|
||||||
user: 1,
|
user: 1,
|
||||||
organization: null,
|
organization: null,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
credential_type: '2',
|
credential_type: '2',
|
||||||
inputs: {},
|
inputs: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
ssh_key_data: '',
|
||||||
|
ssh_key_unlock: '',
|
||||||
|
},
|
||||||
|
passwordPrompts: {},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await sleep(1);
|
|
||||||
expect(CredentialsAPI.update).toHaveBeenCalledWith(3, {
|
expect(CredentialsAPI.update).toHaveBeenCalledWith(3, {
|
||||||
user: 1,
|
user: 1,
|
||||||
organization: null,
|
organization: null,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
credential_type: '2',
|
credential_type: '2',
|
||||||
inputs: {},
|
inputs: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
ssh_key_data: '',
|
||||||
|
ssh_key_unlock: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(history.location.pathname).toBe('/credentials/3/details');
|
expect(history.location.pathname).toBe('/credentials/3/details');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,30 +3,20 @@ import { Formik, useField } from 'formik';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { arrayOf, func, object, shape } from 'prop-types';
|
import { arrayOf, func, object, shape } from 'prop-types';
|
||||||
import { Form, FormGroup, Title } from '@patternfly/react-core';
|
import { Form, FormGroup } from '@patternfly/react-core';
|
||||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
import { required } from '../../../util/validators';
|
import { required } from '../../../util/validators';
|
||||||
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
||||||
import {
|
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||||
FormColumnLayout,
|
import CredentialSubForm from './CredentialSubForm';
|
||||||
SubFormLayout,
|
|
||||||
} from '../../../components/FormLayout';
|
|
||||||
import {
|
|
||||||
GoogleComputeEngineSubForm,
|
|
||||||
ManualSubForm,
|
|
||||||
SourceControlSubForm,
|
|
||||||
} from './CredentialSubForms';
|
|
||||||
|
|
||||||
function CredentialFormFields({
|
function CredentialFormFields({
|
||||||
i18n,
|
i18n,
|
||||||
credentialTypes,
|
credentialTypes,
|
||||||
formik,
|
formik,
|
||||||
gceCredentialTypeId,
|
|
||||||
initialValues,
|
initialValues,
|
||||||
scmCredentialTypeId,
|
|
||||||
sshCredentialTypeId,
|
|
||||||
}) {
|
}) {
|
||||||
const [orgField, orgMeta, orgHelpers] = useField('organization');
|
const [orgField, orgMeta, orgHelpers] = useField('organization');
|
||||||
const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
|
const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
|
||||||
@@ -34,23 +24,52 @@ function CredentialFormFields({
|
|||||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||||
});
|
});
|
||||||
|
|
||||||
const credentialTypeOptions = Object.keys(credentialTypes).map(key => {
|
const credentialTypeOptions = Object.keys(credentialTypes)
|
||||||
return {
|
.map(key => {
|
||||||
value: credentialTypes[key].id,
|
return {
|
||||||
key: credentialTypes[key].kind,
|
value: credentialTypes[key].id,
|
||||||
label: credentialTypes[key].name,
|
key: credentialTypes[key].id,
|
||||||
};
|
label: credentialTypes[key].name,
|
||||||
});
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.label > b.label ? 1 : -1));
|
||||||
|
|
||||||
const resetSubFormFields = (value, form) => {
|
const resetSubFormFields = (newCredentialType, form) => {
|
||||||
Object.keys(form.initialValues.inputs).forEach(label => {
|
credentialTypes[newCredentialType].inputs.fields.forEach(
|
||||||
if (parseInt(value, 10) === form.initialValues.credential_type) {
|
({ ask_at_runtime, type, id, choices, default: defaultValue }) => {
|
||||||
form.setFieldValue(`inputs.${label}`, initialValues.inputs[label]);
|
if (
|
||||||
} else {
|
parseInt(newCredentialType, 10) === form.initialValues.credential_type
|
||||||
form.setFieldValue(`inputs.${label}`, '');
|
) {
|
||||||
|
form.setFieldValue(`inputs.${id}`, initialValues.inputs[id]);
|
||||||
|
if (ask_at_runtime) {
|
||||||
|
form.setFieldValue(
|
||||||
|
`passwordPrompts.${id}`,
|
||||||
|
initialValues.passwordPrompts[id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
form.setFieldValue(`inputs.${id}`, defaultValue || '');
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
form.setFieldValue(`inputs.${id}`, defaultValue || false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choices) {
|
||||||
|
form.setFieldValue(`inputs.${id}`, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ask_at_runtime) {
|
||||||
|
form.setFieldValue(`passwordPrompts.${id}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form.setFieldTouched(`inputs.${id}`, false);
|
||||||
}
|
}
|
||||||
form.setFieldTouched(`inputs.${label}`, false);
|
);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -106,16 +125,9 @@ function CredentialFormFields({
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{credTypeField.value !== undefined && credTypeField.value !== '' && (
|
{credTypeField.value !== undefined && credTypeField.value !== '' && (
|
||||||
<SubFormLayout>
|
<CredentialSubForm
|
||||||
<Title size="md">{i18n._(t`Type Details`)}</Title>
|
credentialType={credentialTypes[credTypeField.value]}
|
||||||
{
|
/>
|
||||||
{
|
|
||||||
[gceCredentialTypeId]: <GoogleComputeEngineSubForm />,
|
|
||||||
[sshCredentialTypeId]: <ManualSubForm />,
|
|
||||||
[scmCredentialTypeId]: <SourceControlSubForm />,
|
|
||||||
}[credTypeField.value]
|
|
||||||
}
|
|
||||||
</SubFormLayout>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -135,19 +147,43 @@ function CredentialForm({
|
|||||||
description: credential.description || '',
|
description: credential.description || '',
|
||||||
organization: credential?.summary_fields?.organization || null,
|
organization: credential?.summary_fields?.organization || null,
|
||||||
credential_type: credential.credential_type || '',
|
credential_type: credential.credential_type || '',
|
||||||
inputs: {
|
inputs: {},
|
||||||
become_method: credential?.inputs?.become_method || '',
|
passwordPrompts: {},
|
||||||
become_password: credential?.inputs?.become_password || '',
|
|
||||||
become_username: credential?.inputs?.become_username || '',
|
|
||||||
password: credential?.inputs?.password || '',
|
|
||||||
project: credential?.inputs?.project || '',
|
|
||||||
ssh_key_data: credential?.inputs?.ssh_key_data || '',
|
|
||||||
ssh_key_unlock: credential?.inputs?.ssh_key_unlock || '',
|
|
||||||
ssh_public_key_data: credential?.inputs?.ssh_public_key_data || '',
|
|
||||||
username: credential?.inputs?.username || '',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Object.values(credentialTypes).forEach(credentialType => {
|
||||||
|
credentialType.inputs.fields.forEach(
|
||||||
|
({ ask_at_runtime, type, id, choices, default: defaultValue }) => {
|
||||||
|
if (credential?.inputs && credential.inputs[id]) {
|
||||||
|
if (ask_at_runtime) {
|
||||||
|
initialValues.passwordPrompts[id] =
|
||||||
|
credential.inputs[id] === 'ASK' || false;
|
||||||
|
}
|
||||||
|
initialValues.inputs[id] = credential.inputs[id];
|
||||||
|
} else {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
initialValues.inputs[id] = defaultValue || '';
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
initialValues.inputs[id] = defaultValue || false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choices) {
|
||||||
|
initialValues.inputs[id] = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ask_at_runtime) {
|
||||||
|
initialValues.passwordPrompts[id] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Object.values(inputSources).forEach(inputSource => {
|
Object.values(inputSources).forEach(inputSource => {
|
||||||
initialValues.inputs[inputSource.input_field_name] = {
|
initialValues.inputs[inputSource.input_field_name] = {
|
||||||
credential: inputSource.summary_fields.source_credential,
|
credential: inputSource.summary_fields.source_credential,
|
||||||
@@ -155,60 +191,10 @@ function CredentialForm({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const scmCredentialTypeId = Object.keys(credentialTypes)
|
|
||||||
.filter(key => credentialTypes[key].namespace === 'scm')
|
|
||||||
.map(key => credentialTypes[key].id)[0];
|
|
||||||
const sshCredentialTypeId = Object.keys(credentialTypes)
|
|
||||||
.filter(key => credentialTypes[key].namespace === 'ssh')
|
|
||||||
.map(key => credentialTypes[key].id)[0];
|
|
||||||
const gceCredentialTypeId = Object.keys(credentialTypes)
|
|
||||||
.filter(key => credentialTypes[key].namespace === 'gce')
|
|
||||||
.map(key => credentialTypes[key].id)[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={values => {
|
onSubmit={values => {
|
||||||
const scmKeys = [
|
|
||||||
'username',
|
|
||||||
'password',
|
|
||||||
'ssh_key_data',
|
|
||||||
'ssh_key_unlock',
|
|
||||||
];
|
|
||||||
const sshKeys = [
|
|
||||||
'username',
|
|
||||||
'password',
|
|
||||||
'ssh_key_data',
|
|
||||||
'ssh_public_key_data',
|
|
||||||
'ssh_key_unlock',
|
|
||||||
'become_method',
|
|
||||||
'become_username',
|
|
||||||
'become_password',
|
|
||||||
];
|
|
||||||
const gceKeys = ['username', 'ssh_key_data', 'project'];
|
|
||||||
if (parseInt(values.credential_type, 10) === scmCredentialTypeId) {
|
|
||||||
Object.keys(values.inputs).forEach(key => {
|
|
||||||
if (scmKeys.indexOf(key) < 0) {
|
|
||||||
delete values.inputs[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
parseInt(values.credential_type, 10) === sshCredentialTypeId
|
|
||||||
) {
|
|
||||||
Object.keys(values.inputs).forEach(key => {
|
|
||||||
if (sshKeys.indexOf(key) < 0) {
|
|
||||||
delete values.inputs[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
parseInt(values.credential_type, 10) === gceCredentialTypeId
|
|
||||||
) {
|
|
||||||
Object.keys(values.inputs).forEach(key => {
|
|
||||||
if (gceKeys.indexOf(key) < 0) {
|
|
||||||
delete values.inputs[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onSubmit(values);
|
onSubmit(values);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -219,9 +205,6 @@ function CredentialForm({
|
|||||||
formik={formik}
|
formik={formik}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
credentialTypes={credentialTypes}
|
credentialTypes={credentialTypes}
|
||||||
gceCredentialTypeId={gceCredentialTypeId}
|
|
||||||
scmCredentialTypeId={scmCredentialTypeId}
|
|
||||||
sshCredentialTypeId={sshCredentialTypeId}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
<FormSubmitError error={submitError} />
|
<FormSubmitError error={submitError} />
|
||||||
@@ -239,13 +222,16 @@ function CredentialForm({
|
|||||||
CredentialForm.proptype = {
|
CredentialForm.proptype = {
|
||||||
handleSubmit: func.isRequired,
|
handleSubmit: func.isRequired,
|
||||||
handleCancel: func.isRequired,
|
handleCancel: func.isRequired,
|
||||||
|
credentialTypes: shape({}).isRequired,
|
||||||
credential: shape({}),
|
credential: shape({}),
|
||||||
inputSources: arrayOf(object),
|
inputSources: arrayOf(object),
|
||||||
|
submitError: shape({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
CredentialForm.defaultProps = {
|
CredentialForm.defaultProps = {
|
||||||
credential: {},
|
credential: {},
|
||||||
inputSources: [],
|
inputSources: [],
|
||||||
|
submitError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(CredentialForm);
|
export default withI18n()(CredentialForm);
|
||||||
|
|||||||
@@ -4,11 +4,19 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
|||||||
import machineCredential from './data.machineCredential.json';
|
import machineCredential from './data.machineCredential.json';
|
||||||
import gceCredential from './data.gceCredential.json';
|
import gceCredential from './data.gceCredential.json';
|
||||||
import scmCredential from './data.scmCredential.json';
|
import scmCredential from './data.scmCredential.json';
|
||||||
import credentialTypes from './data.credentialTypes.json';
|
import credentialTypesArr from './data.credentialTypes.json';
|
||||||
import CredentialForm from './CredentialForm';
|
import CredentialForm from './CredentialForm';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
|
|
||||||
|
const credentialTypes = credentialTypesArr.reduce(
|
||||||
|
(credentialTypesMap, credentialType) => {
|
||||||
|
credentialTypesMap[credentialType.id] = credentialType;
|
||||||
|
return credentialTypesMap;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
describe('<CredentialForm />', () => {
|
describe('<CredentialForm />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const onCancel = jest.fn();
|
const onCancel = jest.fn();
|
||||||
@@ -28,23 +36,19 @@ describe('<CredentialForm />', () => {
|
|||||||
expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
|
expect(wrapper.find('input#credential-password').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('FormGroup[label="Signed SSH Certificate"]').length
|
wrapper.find('FormGroup[label="Signed SSH Certificate"]').length
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
|
expect(wrapper.find('input#credential-ssh_key_unlock').length).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('FormGroup[label="Private Key Passphrase"]').length
|
wrapper.find('FormGroup[label="Privilege Escalation Method"]').length
|
||||||
).toBe(1);
|
|
||||||
expect(
|
|
||||||
wrapper.find('FormGroup[label="Privelege Escalation Method"]').length
|
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('FormGroup[label="Privilege Escalation Username"]').length
|
wrapper.find('FormGroup[label="Privilege Escalation Username"]').length
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(
|
expect(wrapper.find('input#credential-become_password').length).toBe(1);
|
||||||
wrapper.find('FormGroup[label="Privilege Escalation Password"]').length
|
|
||||||
).toBe(1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceFieldExpects = () => {
|
const sourceFieldExpects = () => {
|
||||||
@@ -55,7 +59,7 @@ describe('<CredentialForm />', () => {
|
|||||||
expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="SCM Private Key"]').length).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('FormGroup[label="Private Key Passphrase"]').length
|
wrapper.find('FormGroup[label="Private Key Passphrase"]').length
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
@@ -71,10 +75,10 @@ describe('<CredentialForm />', () => {
|
|||||||
wrapper.find('FormGroup[label="Service account JSON file"]').length
|
wrapper.find('FormGroup[label="Service account JSON file"]').length
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('FormGroup[label="Service account email address"]').length
|
wrapper.find('FormGroup[label="Service Account Email Address"]').length
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="Project"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Project"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="RSA private key"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="RSA Private Key"]').length).toBe(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Add', () => {
|
describe('Add', () => {
|
||||||
@@ -152,9 +156,9 @@ describe('<CredentialForm />', () => {
|
|||||||
gceFieldExpects();
|
gceFieldExpects();
|
||||||
expect(wrapper.find('input#credential-username').prop('value')).toBe('');
|
expect(wrapper.find('input#credential-username').prop('value')).toBe('');
|
||||||
expect(wrapper.find('input#credential-project').prop('value')).toBe('');
|
expect(wrapper.find('input#credential-project').prop('value')).toBe('');
|
||||||
expect(wrapper.find('textarea#credential-sshKeyData').prop('value')).toBe(
|
expect(
|
||||||
''
|
wrapper.find('textarea#credential-ssh_key_data').prop('value')
|
||||||
);
|
).toBe('');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('FileUpload').invoke('onChange')({
|
wrapper.find('FileUpload').invoke('onChange')({
|
||||||
name: 'foo.json',
|
name: 'foo.json',
|
||||||
@@ -169,7 +173,9 @@ describe('<CredentialForm />', () => {
|
|||||||
expect(wrapper.find('input#credential-project').prop('value')).toBe(
|
expect(wrapper.find('input#credential-project').prop('value')).toBe(
|
||||||
'test123'
|
'test123'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('textarea#credential-sshKeyData').prop('value')).toBe(
|
expect(
|
||||||
|
wrapper.find('textarea#credential-ssh_key_data').prop('value')
|
||||||
|
).toBe(
|
||||||
'-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----\n'
|
'-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----\n'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -180,9 +186,9 @@ describe('<CredentialForm />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('input#credential-username').prop('value')).toBe('');
|
expect(wrapper.find('input#credential-username').prop('value')).toBe('');
|
||||||
expect(wrapper.find('input#credential-project').prop('value')).toBe('');
|
expect(wrapper.find('input#credential-project').prop('value')).toBe('');
|
||||||
expect(wrapper.find('textarea#credential-sshKeyData').prop('value')).toBe(
|
expect(
|
||||||
''
|
wrapper.find('textarea#credential-ssh_key_data').prop('value')
|
||||||
);
|
).toBe('');
|
||||||
});
|
});
|
||||||
test('should show error when error thrown parsing JSON', async () => {
|
test('should show error when error thrown parsing JSON', async () => {
|
||||||
expect(wrapper.find('#credential-gce-file-helper').text()).toBe(
|
expect(wrapper.find('#credential-gce-file-helper').text()).toBe(
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { bool, shape, string } from 'prop-types';
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { FieldTooltip } from '../../../../components/FormField';
|
||||||
|
|
||||||
|
function BecomeMethodField({ fieldOptions, isRequired }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [options, setOptions] = useState(
|
||||||
|
[
|
||||||
|
'sudo',
|
||||||
|
'su',
|
||||||
|
'pbrun',
|
||||||
|
'pfexec',
|
||||||
|
'dzdo',
|
||||||
|
'pmrun',
|
||||||
|
'runas',
|
||||||
|
'enable',
|
||||||
|
'doas',
|
||||||
|
'ksu',
|
||||||
|
'machinectl',
|
||||||
|
'sesu',
|
||||||
|
].map(val => ({ value: val }))
|
||||||
|
);
|
||||||
|
const [becomeMethodField, meta, helpers] = useField({
|
||||||
|
name: `inputs.${fieldOptions.id}`,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={`credential-${fieldOptions.id}`}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
label={fieldOptions.label}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isValid={!(meta.touched && meta.error)}
|
||||||
|
>
|
||||||
|
{fieldOptions.help_text && (
|
||||||
|
<FieldTooltip content={fieldOptions.help_text} />
|
||||||
|
)}
|
||||||
|
<Select
|
||||||
|
maxHeight={200}
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
onToggle={setIsOpen}
|
||||||
|
onClear={() => {
|
||||||
|
helpers.setValue('');
|
||||||
|
}}
|
||||||
|
onSelect={(event, option) => {
|
||||||
|
helpers.setValue(option);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
isExpanded={isOpen}
|
||||||
|
selections={becomeMethodField.value}
|
||||||
|
isCreatable
|
||||||
|
onCreateOption={option => {
|
||||||
|
setOptions([...options, { value: option }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{options.map(option => (
|
||||||
|
<SelectOption key={option.value} value={option.value} />
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
BecomeMethodField.propTypes = {
|
||||||
|
fieldOptions: shape({
|
||||||
|
id: string.isRequired,
|
||||||
|
label: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
isRequired: bool,
|
||||||
|
};
|
||||||
|
BecomeMethodField.defaultProps = {
|
||||||
|
isRequired: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BecomeMethodField;
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useField, useFormikContext } from 'formik';
|
||||||
|
import { shape, string } from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
TextArea,
|
||||||
|
TextInput,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { FieldTooltip, PasswordInput } from '../../../../components/FormField';
|
||||||
|
import AnsibleSelect from '../../../../components/AnsibleSelect';
|
||||||
|
import { CredentialType } from '../../../../types';
|
||||||
|
import { required } from '../../../../util/validators';
|
||||||
|
import { CredentialPluginField } from './CredentialPlugins';
|
||||||
|
import BecomeMethodField from './BecomeMethodField';
|
||||||
|
|
||||||
|
function CredentialInput({ fieldOptions, credentialKind, ...rest }) {
|
||||||
|
const [subFormField, meta] = useField(`inputs.${fieldOptions.id}`);
|
||||||
|
const isValid = !(meta.touched && meta.error);
|
||||||
|
if (fieldOptions.multiline) {
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
{...subFormField}
|
||||||
|
id={`credential-${fieldOptions.id}`}
|
||||||
|
rows={6}
|
||||||
|
resizeOrientation="vertical"
|
||||||
|
onChange={(value, event) => {
|
||||||
|
subFormField.onChange(event);
|
||||||
|
}}
|
||||||
|
isValid={isValid}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fieldOptions.secret) {
|
||||||
|
const passwordInput = () => (
|
||||||
|
<PasswordInput
|
||||||
|
{...subFormField}
|
||||||
|
id={`credential-${fieldOptions.id}`}
|
||||||
|
isValid={isValid}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return credentialKind === 'external' ? (
|
||||||
|
<InputGroup>{passwordInput()}</InputGroup>
|
||||||
|
) : (
|
||||||
|
passwordInput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...subFormField}
|
||||||
|
id={`credential-${fieldOptions.id}`}
|
||||||
|
onChange={(value, event) => {
|
||||||
|
subFormField.onChange(event);
|
||||||
|
}}
|
||||||
|
isValid={isValid}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CredentialInput.propTypes = {
|
||||||
|
fieldOptions: shape({
|
||||||
|
id: string.isRequired,
|
||||||
|
label: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
credentialKind: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
CredentialInput.defaultProps = {
|
||||||
|
credentialKind: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function CredentialField({ credentialType, fieldOptions, i18n }) {
|
||||||
|
const { values: formikValues } = useFormikContext();
|
||||||
|
const requiredFields = credentialType?.inputs?.required || [];
|
||||||
|
const isRequired = requiredFields.includes(fieldOptions.id);
|
||||||
|
const [subFormField, meta, helpers] = useField({
|
||||||
|
name: `inputs.${fieldOptions.id}`,
|
||||||
|
validate:
|
||||||
|
isRequired && !formikValues?.passwordPrompts[fieldOptions.id]
|
||||||
|
? required(
|
||||||
|
fieldOptions.ask_at_runtime
|
||||||
|
? i18n._(
|
||||||
|
t`Provide a value for this field or select the Prompt on launch option.`
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
i18n
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
const isValid = !(meta.touched && meta.error);
|
||||||
|
|
||||||
|
if (fieldOptions.choices) {
|
||||||
|
const selectOptions = fieldOptions.choices.map(choice => {
|
||||||
|
return {
|
||||||
|
value: choice,
|
||||||
|
key: choice,
|
||||||
|
label: choice,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={`credential-${fieldOptions.id}`}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
label={fieldOptions.label}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isValid={isValid}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
{...subFormField}
|
||||||
|
id="credential_type"
|
||||||
|
data={selectOptions}
|
||||||
|
onChange={(event, value) => {
|
||||||
|
helpers.setValue(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (credentialType.kind === 'external') {
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={`credential-${fieldOptions.id}`}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
label={fieldOptions.label}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isValid={isValid}
|
||||||
|
>
|
||||||
|
{fieldOptions.help_text && (
|
||||||
|
<FieldTooltip content={fieldOptions.help_text} />
|
||||||
|
)}
|
||||||
|
<CredentialInput
|
||||||
|
credentialKind={credentialType.kind}
|
||||||
|
fieldOptions={fieldOptions}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (credentialType.kind === 'ssh' && fieldOptions.id === 'become_method') {
|
||||||
|
return (
|
||||||
|
<BecomeMethodField fieldOptions={fieldOptions} isRequired={isRequired} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CredentialPluginField
|
||||||
|
fieldOptions={fieldOptions}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isValid={isValid}
|
||||||
|
>
|
||||||
|
<CredentialInput fieldOptions={fieldOptions} />
|
||||||
|
</CredentialPluginField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CredentialField.propTypes = {
|
||||||
|
credentialType: CredentialType.isRequired,
|
||||||
|
fieldOptions: shape({
|
||||||
|
id: string.isRequired,
|
||||||
|
label: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CredentialField.defaultProps = {};
|
||||||
|
|
||||||
|
export default withI18n()(CredentialField);
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonVariant,
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
Tooltip,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { KeyIcon } from '@patternfly/react-icons';
|
||||||
|
import { FieldTooltip } from '../../../../../components/FormField';
|
||||||
|
import FieldWithPrompt from '../../../../../components/FieldWithPrompt';
|
||||||
|
import { CredentialPluginPrompt } from './CredentialPluginPrompt';
|
||||||
|
import CredentialPluginSelected from './CredentialPluginSelected';
|
||||||
|
|
||||||
|
function CredentialPluginInput(props) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
i18n,
|
||||||
|
isDisabled,
|
||||||
|
isRequired,
|
||||||
|
isValid,
|
||||||
|
fieldOptions,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [showPluginWizard, setShowPluginWizard] = useState(false);
|
||||||
|
const [inputField, , helpers] = useField(`inputs.${fieldOptions.id}`);
|
||||||
|
const [passwordPromptField] = useField(`passwordPrompts.${fieldOptions.id}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{inputField?.value?.credential ? (
|
||||||
|
<CredentialPluginSelected
|
||||||
|
credential={inputField?.value?.credential}
|
||||||
|
onClearPlugin={() => helpers.setValue('')}
|
||||||
|
onEditPlugin={() => setShowPluginWizard(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InputGroup>
|
||||||
|
{React.cloneElement(children, {
|
||||||
|
...inputField,
|
||||||
|
isRequired,
|
||||||
|
isValid,
|
||||||
|
isDisabled: !!passwordPromptField.value,
|
||||||
|
onChange: (_, event) => {
|
||||||
|
inputField.onChange(event);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
<Tooltip
|
||||||
|
content={i18n._(
|
||||||
|
t`Populate field from an external secret management system`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.control}
|
||||||
|
aria-label={i18n._(
|
||||||
|
t`Populate field from an external secret management system`
|
||||||
|
)}
|
||||||
|
onClick={() => setShowPluginWizard(true)}
|
||||||
|
isDisabled={isDisabled || !!passwordPromptField.value}
|
||||||
|
>
|
||||||
|
<KeyIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</InputGroup>
|
||||||
|
)}
|
||||||
|
{showPluginWizard && (
|
||||||
|
<CredentialPluginPrompt
|
||||||
|
initialValues={
|
||||||
|
typeof inputField.value === 'object' ? inputField.value : {}
|
||||||
|
}
|
||||||
|
onClose={() => setShowPluginWizard(false)}
|
||||||
|
onSubmit={val => {
|
||||||
|
val.touched = true;
|
||||||
|
helpers.setValue(val);
|
||||||
|
setShowPluginWizard(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CredentialPluginField(props) {
|
||||||
|
const { fieldOptions, isRequired, isValid } = props;
|
||||||
|
|
||||||
|
const [, meta, helpers] = useField(`inputs.${fieldOptions.id}`);
|
||||||
|
const [passwordPromptField] = useField(`passwordPrompts.${fieldOptions.id}`);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (passwordPromptField.value) {
|
||||||
|
helpers.setValue('');
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [passwordPromptField.value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{fieldOptions.ask_at_runtime ? (
|
||||||
|
<FieldWithPrompt
|
||||||
|
fieldId={`credential-${fieldOptions.id}`}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={fieldOptions.label}
|
||||||
|
promptId={`credential-prompt-${fieldOptions.id}`}
|
||||||
|
promptName={`passwordPrompts.${fieldOptions.id}`}
|
||||||
|
tooltip={fieldOptions.help_text}
|
||||||
|
>
|
||||||
|
<CredentialPluginInput {...props} />
|
||||||
|
{meta.error && meta.touched && (
|
||||||
|
<div
|
||||||
|
className="pf-c-form__helper-text pf-m-error"
|
||||||
|
id={`${fieldOptions.id}-helper`}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{meta.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FieldWithPrompt>
|
||||||
|
) : (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={`credential-${fieldOptions.id}`}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
isValid={isValid}
|
||||||
|
label={fieldOptions.label}
|
||||||
|
>
|
||||||
|
{fieldOptions.help_text && (
|
||||||
|
<FieldTooltip content={fieldOptions.help_text} />
|
||||||
|
)}
|
||||||
|
<CredentialPluginInput {...props} />
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CredentialPluginField.propTypes = {
|
||||||
|
fieldOptions: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isRequired: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
CredentialPluginField.defaultProps = {
|
||||||
|
isDisabled: false,
|
||||||
|
isRequired: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(CredentialPluginField);
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { TextInput } from '@patternfly/react-core';
|
import { TextInput } from '@patternfly/react-core';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
||||||
import CredentialPluginField from './CredentialPluginField';
|
import CredentialPluginField from './CredentialPluginField';
|
||||||
|
|
||||||
|
const fieldOptions = {
|
||||||
|
id: 'username',
|
||||||
|
label: 'Username',
|
||||||
|
type: 'string',
|
||||||
|
};
|
||||||
|
|
||||||
describe('<CredentialPluginField />', () => {
|
describe('<CredentialPluginField />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
describe('No plugin configured', () => {
|
describe('No plugin configured', () => {
|
||||||
@@ -18,9 +24,9 @@ describe('<CredentialPluginField />', () => {
|
|||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<CredentialPluginField
|
<CredentialPluginField
|
||||||
id="credential-username"
|
fieldOptions={fieldOptions}
|
||||||
name="inputs.username"
|
isDisabled={false}
|
||||||
label="Username"
|
isRequired={false}
|
||||||
>
|
>
|
||||||
<TextInput id="credential-username" />
|
<TextInput id="credential-username" />
|
||||||
</CredentialPluginField>
|
</CredentialPluginField>
|
||||||
@@ -62,9 +68,9 @@ describe('<CredentialPluginField />', () => {
|
|||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<CredentialPluginField
|
<CredentialPluginField
|
||||||
id="credential-username"
|
fieldOptions={fieldOptions}
|
||||||
name="inputs.username"
|
isDisabled={false}
|
||||||
label="Username"
|
isRequired={false}
|
||||||
>
|
>
|
||||||
<TextInput id="credential-username" />
|
<TextInput id="credential-username" />
|
||||||
</CredentialPluginField>
|
</CredentialPluginField>
|
||||||
@@ -3,15 +3,15 @@ import { act } from 'react-dom/test-utils';
|
|||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../../../testUtils/enzymeHelpers';
|
} from '../../../../../../../testUtils/enzymeHelpers';
|
||||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../../../api';
|
import { CredentialsAPI, CredentialTypesAPI } from '../../../../../../api';
|
||||||
import selectedCredential from '../../data.cyberArkCredential.json';
|
import selectedCredential from '../../../data.cyberArkCredential.json';
|
||||||
import azureVaultCredential from '../../data.azureVaultCredential.json';
|
import azureVaultCredential from '../../../data.azureVaultCredential.json';
|
||||||
import hashiCorpCredential from '../../data.hashiCorpCredential.json';
|
import hashiCorpCredential from '../../../data.hashiCorpCredential.json';
|
||||||
import CredentialPluginPrompt from './CredentialPluginPrompt';
|
import CredentialPluginPrompt from './CredentialPluginPrompt';
|
||||||
|
|
||||||
jest.mock('../../../../../api/models/Credentials');
|
jest.mock('../../../../../../api/models/Credentials');
|
||||||
jest.mock('../../../../../api/models/CredentialTypes');
|
jest.mock('../../../../../../api/models/CredentialTypes');
|
||||||
|
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
@@ -3,13 +3,13 @@ import { useHistory } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { CredentialsAPI } from '../../../../../api';
|
import { CredentialsAPI } from '../../../../../../api';
|
||||||
import CheckboxListItem from '../../../../../components/CheckboxListItem';
|
import CheckboxListItem from '../../../../../../components/CheckboxListItem';
|
||||||
import ContentError from '../../../../../components/ContentError';
|
import ContentError from '../../../../../../components/ContentError';
|
||||||
import DataListToolbar from '../../../../../components/DataListToolbar';
|
import DataListToolbar from '../../../../../../components/DataListToolbar';
|
||||||
import PaginatedDataList from '../../../../../components/PaginatedDataList';
|
import PaginatedDataList from '../../../../../../components/PaginatedDataList';
|
||||||
import { getQSConfig, parseQueryString } from '../../../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../../../../util/qs';
|
||||||
import useRequest from '../../../../../util/useRequest';
|
import useRequest from '../../../../../../util/useRequest';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('credential', {
|
const QS_CONFIG = getQSConfig('credential', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -5,14 +5,14 @@ import { useField, useFormikContext } from 'formik';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Button, Form, FormGroup, Tooltip } from '@patternfly/react-core';
|
import { Button, Form, FormGroup, Tooltip } from '@patternfly/react-core';
|
||||||
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
||||||
import { CredentialTypesAPI } from '../../../../../api';
|
import { CredentialTypesAPI } from '../../../../../../api';
|
||||||
import AnsibleSelect from '../../../../../components/AnsibleSelect';
|
import AnsibleSelect from '../../../../../../components/AnsibleSelect';
|
||||||
import ContentError from '../../../../../components/ContentError';
|
import ContentError from '../../../../../../components/ContentError';
|
||||||
import ContentLoading from '../../../../../components/ContentLoading';
|
import ContentLoading from '../../../../../../components/ContentLoading';
|
||||||
import FormField from '../../../../../components/FormField';
|
import FormField from '../../../../../../components/FormField';
|
||||||
import { FormFullWidthLayout } from '../../../../../components/FormLayout';
|
import { FormFullWidthLayout } from '../../../../../../components/FormLayout';
|
||||||
import useRequest from '../../../../../util/useRequest';
|
import useRequest from '../../../../../../util/useRequest';
|
||||||
import { required } from '../../../../../util/validators';
|
import { required } from '../../../../../../util/validators';
|
||||||
|
|
||||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
@@ -5,8 +5,8 @@ import { t, Trans } from '@lingui/macro';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Button, ButtonVariant, Tooltip } from '@patternfly/react-core';
|
import { Button, ButtonVariant, Tooltip } from '@patternfly/react-core';
|
||||||
import { KeyIcon } from '@patternfly/react-icons';
|
import { KeyIcon } from '@patternfly/react-icons';
|
||||||
import CredentialChip from '../../../../components/CredentialChip';
|
import CredentialChip from '../../../../../components/CredentialChip';
|
||||||
import { Credential } from '../../../../types';
|
import { Credential } from '../../../../../types';
|
||||||
|
|
||||||
const SelectedCredential = styled.div`
|
const SelectedCredential = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
||||||
import selectedCredential from '../data.cyberArkCredential.json';
|
import selectedCredential from '../../data.cyberArkCredential.json';
|
||||||
import CredentialPluginSelected from './CredentialPluginSelected';
|
import CredentialPluginSelected from './CredentialPluginSelected';
|
||||||
|
|
||||||
describe('<CredentialPluginSelected />', () => {
|
describe('<CredentialPluginSelected />', () => {
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { FileUpload, FormGroup } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function GceFileUploadField({ i18n }) {
|
||||||
|
const [fileError, setFileError] = useState(null);
|
||||||
|
const [filename, setFilename] = useState('');
|
||||||
|
const [file, setFile] = useState('');
|
||||||
|
const [, , inputsUsernameHelpers] = useField({
|
||||||
|
name: 'inputs.username',
|
||||||
|
});
|
||||||
|
const [, , inputsProjectHelpers] = useField({
|
||||||
|
name: 'inputs.project',
|
||||||
|
});
|
||||||
|
const [, , inputsSSHKeyDataHelpers] = useField({
|
||||||
|
name: 'inputs.ssh_key_data',
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="credential-gce-file"
|
||||||
|
isValid={!fileError}
|
||||||
|
label={i18n._(t`Service account JSON file`)}
|
||||||
|
helperText={i18n._(
|
||||||
|
t`Select a JSON formatted service account key to autopopulate the following fields.`
|
||||||
|
)}
|
||||||
|
helperTextInvalid={fileError}
|
||||||
|
>
|
||||||
|
<FileUpload
|
||||||
|
id="credential-gce-file"
|
||||||
|
value={file}
|
||||||
|
filename={filename}
|
||||||
|
filenamePlaceholder={i18n._(t`Choose a .json file`)}
|
||||||
|
onChange={async value => {
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
setFile(value);
|
||||||
|
setFilename(value.name);
|
||||||
|
const fileText = await value.text();
|
||||||
|
const fileJSON = JSON.parse(fileText);
|
||||||
|
if (
|
||||||
|
!fileJSON.client_email &&
|
||||||
|
!fileJSON.project_id &&
|
||||||
|
!fileJSON.private_key
|
||||||
|
) {
|
||||||
|
setFileError(
|
||||||
|
i18n._(
|
||||||
|
t`Expected at least one of client_email, project_id or private_key to be present in the file.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
inputsUsernameHelpers.setValue(fileJSON.client_email || '');
|
||||||
|
inputsProjectHelpers.setValue(fileJSON.project_id || '');
|
||||||
|
inputsSSHKeyDataHelpers.setValue(fileJSON.private_key || '');
|
||||||
|
setFileError(null);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setFileError(
|
||||||
|
i18n._(
|
||||||
|
t`There was an error parsing the file. Please check the file formatting and try again.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFile('');
|
||||||
|
setFilename('');
|
||||||
|
inputsUsernameHelpers.setValue('');
|
||||||
|
inputsProjectHelpers.setValue('');
|
||||||
|
inputsSSHKeyDataHelpers.setValue('');
|
||||||
|
setFileError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dropzoneProps={{
|
||||||
|
accept: '.json',
|
||||||
|
onDropRejected: () => {
|
||||||
|
setFileError(
|
||||||
|
i18n._(
|
||||||
|
t`File upload rejected. Please select a single .json file.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(GceFileUploadField);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as BecomeMethodField } from './BecomeMethodField';
|
||||||
|
export { default as CredentialField } from './CredentialField';
|
||||||
|
export { default as GceFileUploadField } from './GceFileUploadField';
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { useField } from 'formik';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonVariant,
|
|
||||||
FormGroup,
|
|
||||||
InputGroup,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { KeyIcon } from '@patternfly/react-icons';
|
|
||||||
import { CredentialPluginPrompt } from './CredentialPluginPrompt';
|
|
||||||
import CredentialPluginSelected from './CredentialPluginSelected';
|
|
||||||
|
|
||||||
function CredentialPluginField(props) {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
validate,
|
|
||||||
isRequired,
|
|
||||||
isDisabled,
|
|
||||||
i18n,
|
|
||||||
} = props;
|
|
||||||
const [showPluginWizard, setShowPluginWizard] = useState(false);
|
|
||||||
const [field, meta, helpers] = useField({ name, validate });
|
|
||||||
const isValid = !(meta.touched && meta.error);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup
|
|
||||||
fieldId={id}
|
|
||||||
helperTextInvalid={meta.error}
|
|
||||||
isRequired={isRequired}
|
|
||||||
isValid={isValid}
|
|
||||||
label={label}
|
|
||||||
>
|
|
||||||
{field?.value?.credential ? (
|
|
||||||
<CredentialPluginSelected
|
|
||||||
credential={field?.value?.credential}
|
|
||||||
onClearPlugin={() => helpers.setValue('')}
|
|
||||||
onEditPlugin={() => setShowPluginWizard(true)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<InputGroup>
|
|
||||||
{React.cloneElement(children, {
|
|
||||||
...field,
|
|
||||||
isRequired,
|
|
||||||
onChange: (_, event) => {
|
|
||||||
field.onChange(event);
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Tooltip
|
|
||||||
content={i18n._(
|
|
||||||
t`Populate field from an external secret management system`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant={ButtonVariant.control}
|
|
||||||
aria-label={i18n._(
|
|
||||||
t`Populate field from an external secret management system`
|
|
||||||
)}
|
|
||||||
onClick={() => setShowPluginWizard(true)}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
>
|
|
||||||
<KeyIcon />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</InputGroup>
|
|
||||||
)}
|
|
||||||
{showPluginWizard && (
|
|
||||||
<CredentialPluginPrompt
|
|
||||||
initialValues={typeof field.value === 'object' ? field.value : {}}
|
|
||||||
onClose={() => setShowPluginWizard(false)}
|
|
||||||
onSubmit={val => {
|
|
||||||
val.touched = true;
|
|
||||||
helpers.setValue(val);
|
|
||||||
setShowPluginWizard(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CredentialPluginField.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
validate: PropTypes.func,
|
|
||||||
isRequired: PropTypes.bool,
|
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
CredentialPluginField.defaultProps = {
|
|
||||||
validate: () => {},
|
|
||||||
isRequired: false,
|
|
||||||
isDisabled: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(CredentialPluginField);
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { FormGroup, Title } from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
FormCheckboxLayout,
|
||||||
|
FormColumnLayout,
|
||||||
|
FormFullWidthLayout,
|
||||||
|
SubFormLayout,
|
||||||
|
} from '../../../components/FormLayout';
|
||||||
|
import { CheckboxField } from '../../../components/FormField';
|
||||||
|
import { CredentialType } from '../../../types';
|
||||||
|
import { CredentialField, GceFileUploadField } from './CredentialFormFields';
|
||||||
|
|
||||||
|
function CredentialSubForm({ credentialType, i18n }) {
|
||||||
|
const stringFields = credentialType.inputs.fields.filter(
|
||||||
|
fieldOptions => fieldOptions.type === 'string' || fieldOptions.choices
|
||||||
|
);
|
||||||
|
const booleanFields = credentialType.inputs.fields.filter(
|
||||||
|
fieldOptions => fieldOptions.type === 'boolean'
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<SubFormLayout>
|
||||||
|
<Title size="md">{i18n._(t`Type Details`)}</Title>
|
||||||
|
<FormColumnLayout>
|
||||||
|
{credentialType.namespace === 'gce' && <GceFileUploadField />}
|
||||||
|
{stringFields.map(fieldOptions =>
|
||||||
|
fieldOptions.multiline ? (
|
||||||
|
<FormFullWidthLayout key={fieldOptions.id}>
|
||||||
|
<CredentialField
|
||||||
|
credentialType={credentialType}
|
||||||
|
fieldOptions={fieldOptions}
|
||||||
|
/>
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
) : (
|
||||||
|
<CredentialField
|
||||||
|
key={fieldOptions.id}
|
||||||
|
credentialType={credentialType}
|
||||||
|
fieldOptions={fieldOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{booleanFields.length > 0 && (
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
<FormGroup
|
||||||
|
fieldId="credential-checkboxes"
|
||||||
|
label={i18n._(t`Options`)}
|
||||||
|
>
|
||||||
|
<FormCheckboxLayout>
|
||||||
|
{booleanFields.map(fieldOptions => (
|
||||||
|
<CheckboxField
|
||||||
|
id={`credential-${fieldOptions.id}`}
|
||||||
|
key={fieldOptions.id}
|
||||||
|
name={`inputs.${fieldOptions.id}`}
|
||||||
|
label={fieldOptions.label}
|
||||||
|
tooltip={fieldOptions.help_text}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormCheckboxLayout>
|
||||||
|
</FormGroup>
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
)}
|
||||||
|
</FormColumnLayout>
|
||||||
|
</SubFormLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CredentialSubForm.propTypes = {
|
||||||
|
credentialType: CredentialType.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CredentialSubForm.defaultProps = {};
|
||||||
|
|
||||||
|
export default withI18n()(CredentialSubForm);
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { useField } from 'formik';
|
|
||||||
import {
|
|
||||||
FileUpload,
|
|
||||||
FormGroup,
|
|
||||||
TextArea,
|
|
||||||
TextInput,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import {
|
|
||||||
FormColumnLayout,
|
|
||||||
FormFullWidthLayout,
|
|
||||||
} from '../../../../components/FormLayout';
|
|
||||||
import { required } from '../../../../util/validators';
|
|
||||||
import { CredentialPluginField } from '../CredentialPlugins';
|
|
||||||
|
|
||||||
const GoogleComputeEngineSubForm = ({ i18n }) => {
|
|
||||||
const [fileError, setFileError] = useState(null);
|
|
||||||
const [filename, setFilename] = useState('');
|
|
||||||
const [file, setFile] = useState('');
|
|
||||||
const inputsUsernameHelpers = useField({
|
|
||||||
name: 'inputs.username',
|
|
||||||
})[2];
|
|
||||||
const inputsProjectHelpers = useField({
|
|
||||||
name: 'inputs.project',
|
|
||||||
})[2];
|
|
||||||
const inputsSSHKeyDataHelpers = useField({
|
|
||||||
name: 'inputs.ssh_key_data',
|
|
||||||
})[2];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormColumnLayout>
|
|
||||||
<FormGroup
|
|
||||||
fieldId="credential-gce-file"
|
|
||||||
isValid={!fileError}
|
|
||||||
label={i18n._(t`Service account JSON file`)}
|
|
||||||
helperText={i18n._(
|
|
||||||
t`Select a JSON formatted service account key to autopopulate the following fields.`
|
|
||||||
)}
|
|
||||||
helperTextInvalid={fileError}
|
|
||||||
>
|
|
||||||
<FileUpload
|
|
||||||
id="credential-gce-file"
|
|
||||||
value={file}
|
|
||||||
filename={filename}
|
|
||||||
filenamePlaceholder={i18n._(t`Choose a .json file`)}
|
|
||||||
onChange={async value => {
|
|
||||||
if (value) {
|
|
||||||
try {
|
|
||||||
setFile(value);
|
|
||||||
setFilename(value.name);
|
|
||||||
const fileText = await value.text();
|
|
||||||
const fileJSON = JSON.parse(fileText);
|
|
||||||
if (
|
|
||||||
!fileJSON.client_email &&
|
|
||||||
!fileJSON.project_id &&
|
|
||||||
!fileJSON.private_key
|
|
||||||
) {
|
|
||||||
setFileError(
|
|
||||||
i18n._(
|
|
||||||
t`Expected at least one of client_email, project_id or private_key to be present in the file.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
inputsUsernameHelpers.setValue(fileJSON.client_email || '');
|
|
||||||
inputsProjectHelpers.setValue(fileJSON.project_id || '');
|
|
||||||
inputsSSHKeyDataHelpers.setValue(fileJSON.private_key || '');
|
|
||||||
setFileError(null);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setFileError(
|
|
||||||
i18n._(
|
|
||||||
t`There was an error parsing the file. Please check the file formatting and try again.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setFile('');
|
|
||||||
setFilename('');
|
|
||||||
inputsUsernameHelpers.setValue('');
|
|
||||||
inputsProjectHelpers.setValue('');
|
|
||||||
inputsSSHKeyDataHelpers.setValue('');
|
|
||||||
setFileError(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
dropzoneProps={{
|
|
||||||
accept: '.json',
|
|
||||||
onDropRejected: () => {
|
|
||||||
setFileError(
|
|
||||||
i18n._(
|
|
||||||
t`File upload rejected. Please select a single .json file.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<CredentialPluginField
|
|
||||||
id="credential-username"
|
|
||||||
label={i18n._(t`Service account email address`)}
|
|
||||||
name="inputs.username"
|
|
||||||
type="email"
|
|
||||||
validate={required(null, i18n)}
|
|
||||||
isRequired
|
|
||||||
>
|
|
||||||
<TextInput id="credential-username" />
|
|
||||||
</CredentialPluginField>
|
|
||||||
<CredentialPluginField
|
|
||||||
id="credential-project"
|
|
||||||
label={i18n._(t`Project`)}
|
|
||||||
name="inputs.project"
|
|
||||||
>
|
|
||||||
<TextInput id="credential-project" />
|
|
||||||
</CredentialPluginField>
|
|
||||||
<FormFullWidthLayout>
|
|
||||||
<CredentialPluginField
|
|
||||||
id="credential-sshKeyData"
|
|
||||||
label={i18n._(t`RSA private key`)}
|
|
||||||
name="inputs.ssh_key_data"
|
|
||||||
type="textarea"
|
|
||||||
validate={required(null, i18n)}
|
|
||||||
isRequired
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
id="credential-sshKeyData"
|
|
||||||
rows={6}
|
|
||||||
resizeOrientation="vertical"
|
|
||||||
/>
|
|
||||||
</CredentialPluginField>
|
|
||||||
</FormFullWidthLayout>
|
|
||||||
</FormColumnLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(GoogleComputeEngineSubForm);
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useField } from 'formik';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { FormGroup } from '@patternfly/react-core';
|
|
||||||
import FormField, { PasswordField } from '../../../../components/FormField';
|
|
||||||
import AnsibleSelect from '../../../../components/AnsibleSelect';
|
|
||||||
import {
|
|
||||||
FormColumnLayout,
|
|
||||||
FormFullWidthLayout,
|
|
||||||
} from '../../../../components/FormLayout';
|
|
||||||
import {
|
|
||||||
UsernameFormField,
|
|
||||||
PasswordFormField,
|
|
||||||
SSHKeyUnlockField,
|
|
||||||
SSHKeyDataField,
|
|
||||||
} from './SharedFields';
|
|
||||||
|
|
||||||
const ManualSubForm = ({ i18n }) => {
|
|
||||||
const becomeMethodOptions = [
|
|
||||||
{
|
|
||||||
value: '',
|
|
||||||
key: '',
|
|
||||||
label: i18n._(t`Choose a Privelege Escalation Method`),
|
|
||||||
isDisabled: true,
|
|
||||||
},
|
|
||||||
...[
|
|
||||||
'sudo',
|
|
||||||
'su',
|
|
||||||
'pbrun',
|
|
||||||
'pfexec',
|
|
||||||
'dzdo',
|
|
||||||
'pmrun',
|
|
||||||
'runas',
|
|
||||||
'enable',
|
|
||||||
'doas',
|
|
||||||
'ksu',
|
|
||||||
'machinectl',
|
|
||||||
'sesu',
|
|
||||||
].map(val => ({ value: val, key: val, label: val })),
|
|
||||||
];
|
|
||||||
|
|
||||||
const becomeMethodFieldArr = useField('inputs.become_method');
|
|
||||||
const becomeMethodField = becomeMethodFieldArr[0];
|
|
||||||
const becomeMethodHelpers = becomeMethodFieldArr[2];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormColumnLayout>
|
|
||||||
<UsernameFormField />
|
|
||||||
<PasswordFormField />
|
|
||||||
<FormFullWidthLayout>
|
|
||||||
<SSHKeyDataField />
|
|
||||||
<FormField
|
|
||||||
id="credential-sshPublicKeyData"
|
|
||||||
label={i18n._(t`Signed SSH Certificate`)}
|
|
||||||
name="inputs.ssh_public_key_data"
|
|
||||||
type="textarea"
|
|
||||||
/>
|
|
||||||
</FormFullWidthLayout>
|
|
||||||
<SSHKeyUnlockField />
|
|
||||||
<FormGroup
|
|
||||||
fieldId="credential-becomeMethod"
|
|
||||||
label={i18n._(t`Privelege Escalation Method`)}
|
|
||||||
>
|
|
||||||
<AnsibleSelect
|
|
||||||
{...becomeMethodField}
|
|
||||||
id="credential-becomeMethod"
|
|
||||||
data={becomeMethodOptions}
|
|
||||||
onChange={(event, value) => {
|
|
||||||
becomeMethodHelpers.setValue(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormField
|
|
||||||
id="credential-becomeUsername"
|
|
||||||
label={i18n._(t`Privilege Escalation Username`)}
|
|
||||||
name="inputs.become_username"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<PasswordField
|
|
||||||
id="credential-becomePassword"
|
|
||||||
label={i18n._(t`Privilege Escalation Password`)}
|
|
||||||
name="inputs.become_password"
|
|
||||||
/>
|
|
||||||
</FormColumnLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(ManualSubForm);
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { TextArea, TextInput } from '@patternfly/react-core';
|
|
||||||
import { CredentialPluginField } from '../CredentialPlugins';
|
|
||||||
import { PasswordInput } from '../../../../components/FormField';
|
|
||||||
|
|
||||||
export const UsernameFormField = withI18n()(({ i18n }) => (
|
|
||||||
<CredentialPluginField
|
|
||||||
id="credential-username"
|
|
||||||
label={i18n._(t`Username`)}
|
|
||||||
name="inputs.username"
|
|
||||||
>
|
|
||||||
<TextInput id="credential-username" />
|
|
||||||
</CredentialPluginField>
|
|
||||||
));
|
|
||||||
|
|
||||||
export const PasswordFormField = withI18n()(({ i18n }) => (
|
|
||||||
<CredentialPluginField
|
|
||||||
id="credential-password"
|
|
||||||
label={i18n._(t`Password`)}
|
|
||||||
name="inputs.password"
|
|
||||||
>
|
|
||||||
<PasswordInput id="credential-password" />
|
|
||||||
</CredentialPluginField>
|
|
||||||
));
|
|
||||||
|
|
||||||
export const SSHKeyDataField = withI18n()(({ i18n }) => (
|
|
||||||
<CredentialPluginField
|
|
||||||
id="credential-sshKeyData"
|
|
||||||
label={i18n._(t`SSH Private Key`)}
|
|
||||||
name="inputs.ssh_key_data"
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
id="credential-sshKeyData"
|
|
||||||
rows={6}
|
|
||||||
resizeOrientation="vertical"
|
|
||||||
/>
|
|
||||||
</CredentialPluginField>
|
|
||||||
));
|
|
||||||
|
|
||||||
export const SSHKeyUnlockField = withI18n()(({ i18n }) => (
|
|
||||||
<CredentialPluginField
|
|
||||||
id="credential-sshKeyUnlock"
|
|
||||||
label={i18n._(t`Private Key Passphrase`)}
|
|
||||||
name="inputs.ssh_key_unlock"
|
|
||||||
>
|
|
||||||
<PasswordInput id="credential-password" />
|
|
||||||
</CredentialPluginField>
|
|
||||||
));
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import {
|
|
||||||
FormColumnLayout,
|
|
||||||
FormFullWidthLayout,
|
|
||||||
} from '../../../../components/FormLayout';
|
|
||||||
import {
|
|
||||||
UsernameFormField,
|
|
||||||
PasswordFormField,
|
|
||||||
SSHKeyUnlockField,
|
|
||||||
SSHKeyDataField,
|
|
||||||
} from './SharedFields';
|
|
||||||
|
|
||||||
const SourceControlSubForm = () => (
|
|
||||||
<FormColumnLayout>
|
|
||||||
<UsernameFormField />
|
|
||||||
<PasswordFormField />
|
|
||||||
<SSHKeyUnlockField />
|
|
||||||
<FormFullWidthLayout>
|
|
||||||
<SSHKeyDataField />
|
|
||||||
</FormFullWidthLayout>
|
|
||||||
</FormColumnLayout>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default withI18n()(SourceControlSubForm);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { default as GoogleComputeEngineSubForm } from './GoogleComputeEngineSubForm';
|
|
||||||
export { default as ManualSubForm } from './ManualSubForm';
|
|
||||||
export { default as SourceControlSubForm } from './SourceControlSubForm';
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -336,3 +336,16 @@ export const Survey = shape({
|
|||||||
description: string,
|
description: string,
|
||||||
spec: arrayOf(SurveyQuestion),
|
spec: arrayOf(SurveyQuestion),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const CredentialType = shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
type: string.isRequired,
|
||||||
|
url: string.isRequired,
|
||||||
|
related: shape({}),
|
||||||
|
summary_fields: shape({}),
|
||||||
|
name: string.isRequired,
|
||||||
|
description: string,
|
||||||
|
kind: string.isRequired,
|
||||||
|
namespace: string,
|
||||||
|
inputs: shape({}).isRequired,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user