Dynamically render credential subform fields based on options responses for each credential type

This commit is contained in:
mabashian 2020-06-04 14:29:28 -04:00
parent dba55fec47
commit 3dfc9328a9
30 changed files with 2022 additions and 664 deletions

View File

@ -7,16 +7,11 @@ import { CheckboxField, FieldTooltip } from '../FormField';
const FieldHeader = styled.div`
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)`
--pf-c-check__label--FontSize: var(--pf-c-form__label--FontSize);
margin-left: auto;
`;
function FieldWithPrompt({

View File

@ -25,17 +25,32 @@ function CredentialAdd({ me }) {
result: credentialId,
} = useRequest(
useCallback(
async values => {
const { inputs, organization, ...remainingValues } = values;
async (values, credentialTypesMap) => {
const {
inputs: { fields: possibleFields },
} = credentialTypesMap[values.credential_type];
const {
inputs,
organization,
passwordPrompts,
...remainingValues
} = values;
const nonPluginInputs = {};
const pluginInputs = {};
Object.entries(inputs).forEach(([key, value]) => {
if (value.credential && value.inputs) {
pluginInputs[key] = value;
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[key] = value;
nonPluginInputs[field.id] = input;
}
});
const {
data: { id: newCredentialId },
} = await CredentialsAPI.create({
@ -44,18 +59,17 @@ function CredentialAdd({ me }) {
inputs: nonPluginInputs,
...remainingValues,
});
const inputSourceRequests = [];
Object.entries(pluginInputs).forEach(([key, value]) => {
inputSourceRequests.push(
await Promise.all(
Object.entries(pluginInputs).map(([key, value]) =>
CredentialInputSourcesAPI.create({
input_field_name: key,
metadata: value.inputs,
source_credential: value.credential.id,
target_credential: newCredentialId,
})
);
});
await Promise.all(inputSourceRequests);
)
);
return newCredentialId;
},
@ -74,10 +88,13 @@ function CredentialAdd({ me }) {
try {
const {
data: { results: loadedCredentialTypes },
} = await CredentialTypesAPI.read({
or__namespace: ['gce', 'scm', 'ssh'],
});
setCredentialTypes(loadedCredentialTypes);
} = await CredentialTypesAPI.read();
setCredentialTypes(
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
credentialTypesMap[credentialType.id] = credentialType;
return credentialTypesMap;
}, {})
);
} catch (err) {
setError(err);
} finally {
@ -92,7 +109,7 @@ function CredentialAdd({ me }) {
};
const handleSubmit = async values => {
await submitRequest(values);
await submitRequest(values, credentialTypes);
};
if (error) {

View File

@ -5,7 +5,6 @@ import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
import { sleep } from '../../../../testUtils/testUtils';
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
import CredentialAdd from './CredentialAdd';
@ -175,23 +174,34 @@ describe('<CredentialAdd />', () => {
});
test('handleSubmit should call the api and redirect to details page', async () => {
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
wrapper.find('CredentialForm').prop('onSubmit')({
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
inputs: {},
await act(async () => {
wrapper.find('CredentialForm').prop('onSubmit')({
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
inputs: {
username: '',
password: '',
ssh_key_data: '',
ssh_key_unlock: '',
},
passwordPrompts: {},
});
});
await sleep(1);
expect(CredentialsAPI.create).toHaveBeenCalledWith({
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
inputs: {},
inputs: {
username: '',
password: '',
ssh_key_data: '',
ssh_key_unlock: '',
},
});
expect(history.location.pathname).toBe('/credentials/13/details');
});

View File

@ -1,7 +1,6 @@
import React, { useCallback, useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { object } from 'prop-types';
import { CardBody } from '../../../components/Card';
import {
CredentialsAPI,
@ -22,8 +21,33 @@ function CredentialEdit({ credential, me }) {
const { error: submitError, request: submitRequest, result } = useRequest(
useCallback(
async (values, inputSourceMap) => {
const createAndUpdateInputSources = pluginInputs =>
async (values, credentialTypesMap, inputSourceMap) => {
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]) => {
if (!inputSourceMap[fieldName]) {
return CredentialInputSourcesAPI.create({
@ -46,27 +70,15 @@ function CredentialEdit({ credential, me }) {
return null;
});
const destroyInputSources = inputs => {
const destroyRequests = [];
Object.values(inputSourceMap).forEach(inputSource => {
const destroyInputSources = () =>
Object.values(inputSourceMap).map(inputSource => {
const { id, input_field_name } = inputSource;
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([
CredentialsAPI.update(credential.id, {
user: (me && me.id) || null,
@ -74,12 +86,14 @@ function CredentialEdit({ credential, me }) {
inputs: nonPluginInputs,
...remainingValues,
}),
...destroyInputSources(inputs),
...destroyInputSources(),
]);
await Promise.all(createAndUpdateInputSources(pluginInputs));
await Promise.all(createAndUpdateInputSources());
return data;
},
[credential.id, me]
[me, credential.id]
)
);
@ -100,12 +114,15 @@ function CredentialEdit({ credential, me }) {
data: { results: loadedInputSources },
},
] = await Promise.all([
CredentialTypesAPI.read({
or__namespace: ['gce', 'scm', 'ssh'],
}),
CredentialTypesAPI.read(),
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
]);
setCredentialTypes(loadedCredentialTypes);
setCredentialTypes(
loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
credentialTypesMap[credentialType.id] = credentialType;
return credentialTypesMap;
}, {})
);
setInputSources(
loadedInputSources.reduce((inputSourcesMap, inputSource) => {
inputSourcesMap[inputSource.input_field_name] = inputSource;
@ -127,7 +144,7 @@ function CredentialEdit({ credential, me }) {
};
const handleSubmit = async values => {
await submitRequest(values, inputSources);
await submitRequest(values, credentialTypes, inputSources);
};
if (error) {

View File

@ -5,7 +5,6 @@ import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
import { sleep } from '../../../../testUtils/testUtils';
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
import CredentialEdit from './CredentialEdit';
@ -279,23 +278,34 @@ describe('<CredentialEdit />', () => {
test('handleSubmit should post to the api', async () => {
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
wrapper.find('CredentialForm').prop('onSubmit')({
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
inputs: {},
await act(async () => {
wrapper.find('CredentialForm').prop('onSubmit')({
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
inputs: {
username: '',
password: '',
ssh_key_data: '',
ssh_key_unlock: '',
},
passwordPrompts: {},
});
});
await sleep(1);
expect(CredentialsAPI.update).toHaveBeenCalledWith(3, {
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
inputs: {},
inputs: {
username: '',
password: '',
ssh_key_data: '',
ssh_key_unlock: '',
},
});
expect(history.location.pathname).toBe('/credentials/3/details');
});

View File

@ -3,30 +3,20 @@ import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
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 FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
import AnsibleSelect from '../../../components/AnsibleSelect';
import { required } from '../../../util/validators';
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
import {
FormColumnLayout,
SubFormLayout,
} from '../../../components/FormLayout';
import {
GoogleComputeEngineSubForm,
ManualSubForm,
SourceControlSubForm,
} from './CredentialSubForms';
import { FormColumnLayout } from '../../../components/FormLayout';
import CredentialSubForm from './CredentialSubForm';
function CredentialFormFields({
i18n,
credentialTypes,
formik,
gceCredentialTypeId,
initialValues,
scmCredentialTypeId,
sshCredentialTypeId,
}) {
const [orgField, orgMeta, orgHelpers] = useField('organization');
const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
@ -34,23 +24,52 @@ function CredentialFormFields({
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const credentialTypeOptions = Object.keys(credentialTypes).map(key => {
return {
value: credentialTypes[key].id,
key: credentialTypes[key].kind,
label: credentialTypes[key].name,
};
});
const credentialTypeOptions = Object.keys(credentialTypes)
.map(key => {
return {
value: credentialTypes[key].id,
key: credentialTypes[key].id,
label: credentialTypes[key].name,
};
})
.sort((a, b) => (a.label > b.label ? 1 : -1));
const resetSubFormFields = (value, form) => {
Object.keys(form.initialValues.inputs).forEach(label => {
if (parseInt(value, 10) === form.initialValues.credential_type) {
form.setFieldValue(`inputs.${label}`, initialValues.inputs[label]);
} else {
form.setFieldValue(`inputs.${label}`, '');
const resetSubFormFields = (newCredentialType, form) => {
credentialTypes[newCredentialType].inputs.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 (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 (
@ -106,16 +125,9 @@ function CredentialFormFields({
/>
</FormGroup>
{credTypeField.value !== undefined && credTypeField.value !== '' && (
<SubFormLayout>
<Title size="md">{i18n._(t`Type Details`)}</Title>
{
{
[gceCredentialTypeId]: <GoogleComputeEngineSubForm />,
[sshCredentialTypeId]: <ManualSubForm />,
[scmCredentialTypeId]: <SourceControlSubForm />,
}[credTypeField.value]
}
</SubFormLayout>
<CredentialSubForm
credentialType={credentialTypes[credTypeField.value]}
/>
)}
</>
);
@ -135,19 +147,43 @@ function CredentialForm({
description: credential.description || '',
organization: credential?.summary_fields?.organization || null,
credential_type: credential.credential_type || '',
inputs: {
become_method: credential?.inputs?.become_method || '',
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 || '',
},
inputs: {},
passwordPrompts: {},
};
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 => {
initialValues.inputs[inputSource.input_field_name] = {
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 (
<Formik
initialValues={initialValues}
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);
}}
>
@ -219,9 +205,6 @@ function CredentialForm({
formik={formik}
initialValues={initialValues}
credentialTypes={credentialTypes}
gceCredentialTypeId={gceCredentialTypeId}
scmCredentialTypeId={scmCredentialTypeId}
sshCredentialTypeId={sshCredentialTypeId}
{...rest}
/>
<FormSubmitError error={submitError} />
@ -239,13 +222,16 @@ function CredentialForm({
CredentialForm.proptype = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
credentialTypes: shape({}).isRequired,
credential: shape({}),
inputSources: arrayOf(object),
submitError: shape({}),
};
CredentialForm.defaultProps = {
credential: {},
inputSources: [],
submitError: null,
};
export default withI18n()(CredentialForm);

View File

@ -4,11 +4,19 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import machineCredential from './data.machineCredential.json';
import gceCredential from './data.gceCredential.json';
import scmCredential from './data.scmCredential.json';
import credentialTypes from './data.credentialTypes.json';
import credentialTypesArr from './data.credentialTypes.json';
import CredentialForm from './CredentialForm';
jest.mock('../../../api');
const credentialTypes = credentialTypesArr.reduce(
(credentialTypesMap, credentialType) => {
credentialTypesMap[credentialType.id] = credentialType;
return credentialTypesMap;
},
{}
);
describe('<CredentialForm />', () => {
let wrapper;
const onCancel = jest.fn();
@ -28,23 +36,19 @@ describe('<CredentialForm />', () => {
expect(wrapper.find('FormGroup[label="Organization"]').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="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="Signed SSH Certificate"]').length
).toBe(1);
expect(wrapper.find('input#credential-ssh_key_unlock').length).toBe(1);
expect(
wrapper.find('FormGroup[label="Private Key Passphrase"]').length
).toBe(1);
expect(
wrapper.find('FormGroup[label="Privelege Escalation Method"]').length
wrapper.find('FormGroup[label="Privilege Escalation Method"]').length
).toBe(1);
expect(
wrapper.find('FormGroup[label="Privilege Escalation Username"]').length
).toBe(1);
expect(
wrapper.find('FormGroup[label="Privilege Escalation Password"]').length
).toBe(1);
expect(wrapper.find('input#credential-become_password').length).toBe(1);
};
const sourceFieldExpects = () => {
@ -55,7 +59,7 @@ describe('<CredentialForm />', () => {
expect(wrapper.find('FormGroup[label="Credential Type"]').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="SSH Private Key"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="SCM Private Key"]').length).toBe(1);
expect(
wrapper.find('FormGroup[label="Private Key Passphrase"]').length
).toBe(1);
@ -71,10 +75,10 @@ describe('<CredentialForm />', () => {
wrapper.find('FormGroup[label="Service account JSON file"]').length
).toBe(1);
expect(
wrapper.find('FormGroup[label="Service account email address"]').length
wrapper.find('FormGroup[label="Service Account Email Address"]').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', () => {
@ -152,9 +156,9 @@ describe('<CredentialForm />', () => {
gceFieldExpects();
expect(wrapper.find('input#credential-username').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 () => {
wrapper.find('FileUpload').invoke('onChange')({
name: 'foo.json',
@ -169,7 +173,9 @@ describe('<CredentialForm />', () => {
expect(wrapper.find('input#credential-project').prop('value')).toBe(
'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'
);
});
@ -180,9 +186,9 @@ describe('<CredentialForm />', () => {
wrapper.update();
expect(wrapper.find('input#credential-username').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 () => {
expect(wrapper.find('#credential-gce-file-helper').text()).toBe(

View File

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

View File

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

View File

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

View File

@ -1,9 +1,15 @@
import React from 'react';
import { Formik } from 'formik';
import { TextInput } from '@patternfly/react-core';
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
import CredentialPluginField from './CredentialPluginField';
const fieldOptions = {
id: 'username',
label: 'Username',
type: 'string',
};
describe('<CredentialPluginField />', () => {
let wrapper;
describe('No plugin configured', () => {
@ -18,9 +24,9 @@ describe('<CredentialPluginField />', () => {
>
{() => (
<CredentialPluginField
id="credential-username"
name="inputs.username"
label="Username"
fieldOptions={fieldOptions}
isDisabled={false}
isRequired={false}
>
<TextInput id="credential-username" />
</CredentialPluginField>
@ -62,9 +68,9 @@ describe('<CredentialPluginField />', () => {
>
{() => (
<CredentialPluginField
id="credential-username"
name="inputs.username"
label="Username"
fieldOptions={fieldOptions}
isDisabled={false}
isRequired={false}
>
<TextInput id="credential-username" />
</CredentialPluginField>

View File

@ -3,15 +3,15 @@ import { act } from 'react-dom/test-utils';
import {
mountWithContexts,
waitForElement,
} from '../../../../../../testUtils/enzymeHelpers';
import { CredentialsAPI, CredentialTypesAPI } from '../../../../../api';
import selectedCredential from '../../data.cyberArkCredential.json';
import azureVaultCredential from '../../data.azureVaultCredential.json';
import hashiCorpCredential from '../../data.hashiCorpCredential.json';
} from '../../../../../../../testUtils/enzymeHelpers';
import { CredentialsAPI, CredentialTypesAPI } from '../../../../../../api';
import selectedCredential from '../../../data.cyberArkCredential.json';
import azureVaultCredential from '../../../data.azureVaultCredential.json';
import hashiCorpCredential from '../../../data.hashiCorpCredential.json';
import CredentialPluginPrompt from './CredentialPluginPrompt';
jest.mock('../../../../../api/models/Credentials');
jest.mock('../../../../../api/models/CredentialTypes');
jest.mock('../../../../../../api/models/Credentials');
jest.mock('../../../../../../api/models/CredentialTypes');
CredentialsAPI.read.mockResolvedValue({
data: {

View File

@ -3,13 +3,13 @@ import { useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import { CredentialsAPI } from '../../../../../api';
import CheckboxListItem from '../../../../../components/CheckboxListItem';
import ContentError from '../../../../../components/ContentError';
import DataListToolbar from '../../../../../components/DataListToolbar';
import PaginatedDataList from '../../../../../components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '../../../../../util/qs';
import useRequest from '../../../../../util/useRequest';
import { CredentialsAPI } from '../../../../../../api';
import CheckboxListItem from '../../../../../../components/CheckboxListItem';
import ContentError from '../../../../../../components/ContentError';
import DataListToolbar from '../../../../../../components/DataListToolbar';
import PaginatedDataList from '../../../../../../components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '../../../../../../util/qs';
import useRequest from '../../../../../../util/useRequest';
const QS_CONFIG = getQSConfig('credential', {
page: 1,

View File

@ -5,14 +5,14 @@ import { useField, useFormikContext } from 'formik';
import styled from 'styled-components';
import { Button, Form, FormGroup, Tooltip } from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
import { CredentialTypesAPI } from '../../../../../api';
import AnsibleSelect from '../../../../../components/AnsibleSelect';
import ContentError from '../../../../../components/ContentError';
import ContentLoading from '../../../../../components/ContentLoading';
import FormField from '../../../../../components/FormField';
import { FormFullWidthLayout } from '../../../../../components/FormLayout';
import useRequest from '../../../../../util/useRequest';
import { required } from '../../../../../util/validators';
import { CredentialTypesAPI } from '../../../../../../api';
import AnsibleSelect from '../../../../../../components/AnsibleSelect';
import ContentError from '../../../../../../components/ContentError';
import ContentLoading from '../../../../../../components/ContentLoading';
import FormField from '../../../../../../components/FormField';
import { FormFullWidthLayout } from '../../../../../../components/FormLayout';
import useRequest from '../../../../../../util/useRequest';
import { required } from '../../../../../../util/validators';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px;

View File

@ -5,8 +5,8 @@ import { t, Trans } from '@lingui/macro';
import styled from 'styled-components';
import { Button, ButtonVariant, Tooltip } from '@patternfly/react-core';
import { KeyIcon } from '@patternfly/react-icons';
import CredentialChip from '../../../../components/CredentialChip';
import { Credential } from '../../../../types';
import CredentialChip from '../../../../../components/CredentialChip';
import { Credential } from '../../../../../types';
const SelectedCredential = styled.div`
display: flex;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
import selectedCredential from '../data.cyberArkCredential.json';
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
import selectedCredential from '../../data.cyberArkCredential.json';
import CredentialPluginSelected from './CredentialPluginSelected';
describe('<CredentialPluginSelected />', () => {

View File

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

View File

@ -0,0 +1,3 @@
export { default as BecomeMethodField } from './BecomeMethodField';
export { default as CredentialField } from './CredentialField';
export { default as GceFileUploadField } from './GceFileUploadField';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
export { default as GoogleComputeEngineSubForm } from './GoogleComputeEngineSubForm';
export { default as ManualSubForm } from './ManualSubForm';
export { default as SourceControlSubForm } from './SourceControlSubForm';

View File

@ -336,3 +336,16 @@ export const Survey = shape({
description: string,
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,
});