mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Dynamically render credential subform fields based on options responses for each credential type
This commit is contained in:
parent
dba55fec47
commit
3dfc9328a9
@ -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({
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 { 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>
|
||||
@ -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: {
|
||||
@ -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,
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 />', () => {
|
||||
@ -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,
|
||||
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,
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user