Adds basic credential plugin support to relevant fields in the static credential forms.

This commit is contained in:
mabashian 2020-05-21 14:43:19 -04:00
parent 9d42b8f0f2
commit 4b95297bd4
19 changed files with 756 additions and 97 deletions

View File

@ -1,5 +1,6 @@
import AdHocCommands from './models/AdHocCommands';
import Config from './models/Config';
import CredentialInputSources from './models/CredentialInputSources';
import CredentialTypes from './models/CredentialTypes';
import Credentials from './models/Credentials';
import Groups from './models/Groups';
@ -14,8 +15,8 @@ import Labels from './models/Labels';
import Me from './models/Me';
import NotificationTemplates from './models/NotificationTemplates';
import Organizations from './models/Organizations';
import Projects from './models/Projects';
import ProjectUpdates from './models/ProjectUpdates';
import Projects from './models/Projects';
import Root from './models/Root';
import Schedules from './models/Schedules';
import SystemJobs from './models/SystemJobs';
@ -24,14 +25,15 @@ import UnifiedJobTemplates from './models/UnifiedJobTemplates';
import UnifiedJobs from './models/UnifiedJobs';
import Users from './models/Users';
import WorkflowApprovalTemplates from './models/WorkflowApprovalTemplates';
import WorkflowJobs from './models/WorkflowJobs';
import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes';
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
import WorkflowJobs from './models/WorkflowJobs';
const AdHocCommandsAPI = new AdHocCommands();
const ConfigAPI = new Config();
const CredentialsAPI = new Credentials();
const CredentialInputSourcesAPI = new CredentialInputSources();
const CredentialTypesAPI = new CredentialTypes();
const CredentialsAPI = new Credentials();
const GroupsAPI = new Groups();
const HostsAPI = new Hosts();
const InstanceGroupsAPI = new InstanceGroups();
@ -44,8 +46,8 @@ const LabelsAPI = new Labels();
const MeAPI = new Me();
const NotificationTemplatesAPI = new NotificationTemplates();
const OrganizationsAPI = new Organizations();
const ProjectsAPI = new Projects();
const ProjectUpdatesAPI = new ProjectUpdates();
const ProjectsAPI = new Projects();
const RootAPI = new Root();
const SchedulesAPI = new Schedules();
const SystemJobsAPI = new SystemJobs();
@ -54,15 +56,16 @@ const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
const UnifiedJobsAPI = new UnifiedJobs();
const UsersAPI = new Users();
const WorkflowApprovalTemplatesAPI = new WorkflowApprovalTemplates();
const WorkflowJobsAPI = new WorkflowJobs();
const WorkflowJobTemplateNodesAPI = new WorkflowJobTemplateNodes();
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
const WorkflowJobsAPI = new WorkflowJobs();
export {
AdHocCommandsAPI,
ConfigAPI,
CredentialsAPI,
CredentialInputSourcesAPI,
CredentialTypesAPI,
CredentialsAPI,
GroupsAPI,
HostsAPI,
InstanceGroupsAPI,
@ -75,8 +78,8 @@ export {
MeAPI,
NotificationTemplatesAPI,
OrganizationsAPI,
ProjectsAPI,
ProjectUpdatesAPI,
ProjectsAPI,
RootAPI,
SchedulesAPI,
SystemJobsAPI,
@ -85,7 +88,7 @@ export {
UnifiedJobsAPI,
UsersAPI,
WorkflowApprovalTemplatesAPI,
WorkflowJobsAPI,
WorkflowJobTemplateNodesAPI,
WorkflowJobTemplatesAPI,
WorkflowJobsAPI,
};

View File

@ -0,0 +1,10 @@
import Base from '../Base';
class CredentialInputSources extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/credential_input_sources/';
}
}
export default CredentialInputSources;

View File

@ -6,6 +6,7 @@ class Credentials extends Base {
this.baseUrl = '/api/v2/credentials/';
this.readAccessList = this.readAccessList.bind(this);
this.readInputSources = this.readInputSources.bind(this);
}
readAccessList(id, params) {
@ -13,6 +14,12 @@ class Credentials extends Base {
params,
});
}
readInputSources(id, params) {
return this.http.get(`${this.baseUrl}${id}/input_sources/`, {
params,
});
}
}
export default Credentials;

View File

@ -1,29 +1,14 @@
import React, { useState } from 'react';
import React 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,
TextInput,
Tooltip,
} from '@patternfly/react-core';
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
import { FormGroup, InputGroup } from '@patternfly/react-core';
import { PasswordInput } from '.';
function PasswordField(props) {
const { id, name, label, validate, isRequired, isDisabled, i18n } = props;
const [inputType, setInputType] = useState('password');
const [field, meta] = useField({ name, validate });
const { id, name, label, validate, isRequired } = props;
const [, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
const handlePasswordToggle = () => {
setInputType(inputType === 'text' ? 'password' : 'text');
};
return (
<FormGroup
fieldId={id}
@ -33,32 +18,7 @@ function PasswordField(props) {
label={label}
>
<InputGroup>
<Tooltip
content={inputType === 'password' ? i18n._(t`Show`) : i18n._(t`Hide`)}
>
<Button
variant={ButtonVariant.control}
aria-label={i18n._(t`Toggle Password`)}
onClick={handlePasswordToggle}
isDisabled={isDisabled}
>
{inputType === 'password' && <EyeSlashIcon />}
{inputType === 'text' && <EyeIcon />}
</Button>
</Tooltip>
<TextInput
id={id}
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
{...field}
value={field.value === '$encrypted$' ? '' : field.value}
isDisabled={isDisabled}
isRequired={isRequired}
isValid={isValid}
type={inputType}
onChange={(_, event) => {
field.onChange(event);
}}
/>
<PasswordInput {...props} />
</InputGroup>
</FormGroup>
);
@ -79,4 +39,4 @@ PasswordField.defaultProps = {
isDisabled: false,
};
export default withI18n()(PasswordField);
export default PasswordField;

View File

@ -0,0 +1,71 @@
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,
TextInput,
Tooltip,
} from '@patternfly/react-core';
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
function PasswordInput(props) {
const { id, name, validate, isRequired, isDisabled, i18n } = props;
const [inputType, setInputType] = useState('password');
const [field, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
const handlePasswordToggle = () => {
setInputType(inputType === 'text' ? 'password' : 'text');
};
return (
<>
<Tooltip
content={inputType === 'password' ? i18n._(t`Show`) : i18n._(t`Hide`)}
>
<Button
variant={ButtonVariant.control}
aria-label={i18n._(t`Toggle Password`)}
onClick={handlePasswordToggle}
isDisabled={isDisabled}
>
{inputType === 'password' && <EyeSlashIcon />}
{inputType === 'text' && <EyeIcon />}
</Button>
</Tooltip>
<TextInput
id={id}
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
{...field}
value={field.value === '$encrypted$' ? '' : field.value}
isDisabled={isDisabled}
isRequired={isRequired}
isValid={isValid}
type={inputType}
onChange={(_, event) => {
field.onChange(event);
}}
/>
</>
);
}
PasswordInput.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
validate: PropTypes.func,
isRequired: PropTypes.bool,
isDisabled: PropTypes.bool,
};
PasswordInput.defaultProps = {
validate: () => {},
isRequired: false,
isDisabled: false,
};
export default withI18n()(PasswordInput);

View File

@ -2,4 +2,5 @@ export { default } from './FormField';
export { default as CheckboxField } from './CheckboxField';
export { default as FieldTooltip } from './FieldTooltip';
export { default as PasswordField } from './PasswordField';
export { default as PasswordInput } from './PasswordInput';
export { default as FormSubmitError } from './FormSubmitError';

View File

@ -5,7 +5,11 @@ import { CardBody } from '../../../components/Card';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import { CredentialTypesAPI, CredentialsAPI } from '../../../api';
import {
CredentialInputSourcesAPI,
CredentialTypesAPI,
CredentialsAPI,
} from '../../../api';
import CredentialForm from '../shared/CredentialForm';
function CredentialAdd({ me }) {
@ -38,16 +42,41 @@ function CredentialAdd({ me }) {
};
const handleSubmit = async values => {
const { organization, ...remainingValues } = values;
const { inputs, organization, ...remainingValues } = values;
let pluginInputs = [];
const inputEntries = Object.entries(inputs);
for (const [key, value] of inputEntries) {
if (value.credential && value.inputs) {
pluginInputs.push([key, value]);
delete inputs[key];
}
}
setFormSubmitError(null);
try {
const {
data: { id: credentialId },
} = await CredentialsAPI.create({
user: (me && me.id) || null,
organization: (organization && organization.id) || null,
inputs: inputs || {},
...remainingValues,
});
const inputSourceRequests = [];
for (const [key, value] of pluginInputs) {
if (value.credential && value.inputs) {
inputSourceRequests.push(
CredentialInputSourcesAPI.create({
input_field_name: key,
metadata: value.inputs,
source_credential: value.credential.id,
target_credential: credentialId,
})
);
}
}
await Promise.all(inputSourceRequests);
const url = `/credentials/${credentialId}/details`;
history.push(`${url}`);
} catch (err) {

View File

@ -3,7 +3,11 @@ import { useHistory } from 'react-router-dom';
import { object } from 'prop-types';
import { CardBody } from '../../../components/Card';
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
import {
CredentialsAPI,
CredentialInputSourcesAPI,
CredentialTypesAPI,
} from '../../../api';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import CredentialForm from '../shared/CredentialForm';
@ -12,18 +16,32 @@ function CredentialEdit({ credential, me }) {
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [credentialTypes, setCredentialTypes] = useState(null);
const [inputSources, setInputSources] = useState(null);
const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory();
useEffect(() => {
const loadData = async () => {
try {
const {
data: { results: loadedCredentialTypes },
} = await CredentialTypesAPI.read({
or__namespace: ['gce', 'scm', 'ssh'],
});
const [
{
data: { results: loadedCredentialTypes },
},
{
data: { results: loadedInputSources },
},
] = await Promise.all([
CredentialTypesAPI.read({
or__namespace: ['gce', 'scm', 'ssh'],
}),
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
]);
setCredentialTypes(loadedCredentialTypes);
const inputSourcesMap = {};
loadedInputSources.forEach(inputSource => {
inputSourcesMap[inputSource.input_field_name] = inputSource;
});
setInputSources(inputSourcesMap);
} catch (err) {
setError(err);
} finally {
@ -31,7 +49,7 @@ function CredentialEdit({ credential, me }) {
}
};
loadData();
}, []);
}, [credential.id]);
const handleCancel = () => {
const url = `/credentials/${credential.id}/details`;
@ -39,20 +57,62 @@ function CredentialEdit({ credential, me }) {
history.push(`${url}`);
};
const createAndUpdateInputSources = pluginInputs =>
Object.entries(pluginInputs).map(([fieldName, fieldValue]) => {
if (!inputSources[fieldName]) {
return CredentialInputSourcesAPI.create({
input_field_name: fieldName,
metadata: fieldValue.inputs,
source_credential: fieldValue.credential.id,
target_credential: credential.id,
});
} else if (fieldValue.touched) {
return CredentialInputSourcesAPI.update(inputSources[fieldName].id, {
metadata: fieldValue.inputs,
source_credential: fieldValue.credential.id,
});
}
return null;
});
const destroyInputSources = inputs => {
const destroyRequests = [];
Object.values(inputSources).forEach(inputSource => {
const { id, input_field_name } = inputSource;
if (!inputs[input_field_name]?.credential) {
destroyRequests.push(CredentialInputSourcesAPI.destroy(id));
}
});
return destroyRequests;
};
const handleSubmit = async values => {
const { organization, ...remainingValues } = values;
const { inputs, organization, ...remainingValues } = values;
let pluginInputs = {};
const inputEntries = Object.entries(inputs);
for (const [key, value] of inputEntries) {
if (value.credential && value.inputs) {
pluginInputs[key] = value;
delete inputs[key];
}
}
setFormSubmitError(null);
try {
const {
data: { id: credentialId },
} = await CredentialsAPI.update(credential.id, {
user: (me && me.id) || null,
organization: (organization && organization.id) || null,
...remainingValues,
});
const url = `/credentials/${credentialId}/details`;
await Promise.all([
CredentialsAPI.update(credential.id, {
user: (me && me.id) || null,
organization: (organization && organization.id) || null,
inputs: inputs || {},
...remainingValues,
}),
...destroyInputSources(pluginInputs),
]);
await Promise.all(createAndUpdateInputSources(pluginInputs));
const url = `/credentials/${credential.id}/details`;
history.push(`${url}`);
} catch (err) {
console.log(err);
setFormSubmitError(err);
}
};
@ -72,6 +132,7 @@ function CredentialEdit({ credential, me }) {
onSubmit={handleSubmit}
credential={credential}
credentialTypes={credentialTypes}
inputSources={inputSources}
submitError={formSubmitError}
/>
</CardBody>

View File

@ -245,6 +245,7 @@ CredentialTypesAPI.read.mockResolvedValue({
});
CredentialsAPI.update.mockResolvedValue({ data: { id: 3 } });
CredentialsAPI.readInputSources.mockResolvedValue({ data: { results: [] } });
describe('<CredentialEdit />', () => {
let wrapper;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { func, shape } from 'prop-types';
import { arrayOf, func, object, shape } from 'prop-types';
import { Form, FormGroup, Title } from '@patternfly/react-core';
import FormField, { FormSubmitError } from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
@ -124,6 +124,7 @@ function CredentialFormFields({
function CredentialForm({
credential = {},
credentialTypes,
inputSources,
onSubmit,
onCancel,
submitError,
@ -147,6 +148,13 @@ function CredentialForm({
},
};
Object.values(inputSources).forEach(inputSource => {
initialValues.inputs[inputSource.input_field_name] = {
credential: inputSource.summary_fields.source_credential,
inputs: inputSource.metadata,
};
});
const scmCredentialTypeId = Object.keys(credentialTypes)
.filter(key => credentialTypes[key].namespace === 'scm')
.map(key => credentialTypes[key].id)[0];
@ -232,10 +240,12 @@ CredentialForm.proptype = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
credential: shape({}),
inputSources: arrayOf(object),
};
CredentialForm.defaultProps = {
credential: {},
inputSources: [],
};
export default withI18n()(CredentialForm);

View File

@ -0,0 +1,103 @@
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 '.';
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={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,60 @@
import React from 'react';
import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Wizard } from '@patternfly/react-core';
import { CredentialsStep, MetadataStep } from './';
function CredentialPluginWizard({ i18n, handleSubmit, onClose }) {
const [selectedCredential] = useField('credential');
const steps = [
{
id: 1,
name: i18n._(t`Credential`),
component: <CredentialsStep />,
},
{
id: 2,
name: i18n._(t`Metadata`),
component: <MetadataStep />,
canJumpTo: !!selectedCredential.value,
nextButtonText: i18n._(t`OK`),
},
];
return (
<Wizard
isOpen
onClose={onClose}
title={i18n._(t`External Secret Management System`)}
steps={steps}
onSave={handleSubmit}
></Wizard>
);
}
function CredentialPluginPrompt({ i18n, onClose, onSubmit, initialValues }) {
return (
<Formik
initialValues={{
credential: initialValues?.credential || null,
inputs: initialValues?.inputs || {},
}}
onSubmit={onSubmit}
>
{({ handleSubmit }) => (
<CredentialPluginWizard
handleSubmit={handleSubmit}
i18n={i18n}
onClose={onClose}
/>
)}
</Formik>
);
}
CredentialPluginPrompt.propTypes = {};
CredentialPluginPrompt.defaultProps = {};
export default withI18n()(CredentialPluginPrompt);

View File

@ -0,0 +1,101 @@
import React, { useCallback, useEffect } from 'react';
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';
const QS_CONFIG = getQSConfig('credential', {
page: 1,
page_size: 5,
order_by: 'name',
credential_type__kind: 'external',
});
function CredentialsStep({ i18n }) {
const [selectedCredential, , selectedCredentialHelper] = useField(
'credential'
);
const history = useHistory();
const {
result: { credentials, count },
error: credentialsError,
isLoading: isCredentialsLoading,
request: fetchCredentials,
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search);
const { data } = await CredentialsAPI.read({
...params,
});
return {
credentials: data.results,
count: data.count,
};
}, [history.location.search]),
{ credentials: [], count: 0 }
);
useEffect(() => {
fetchCredentials();
}, [fetchCredentials]);
if (credentialsError) {
return <ContentError error={credentialsError} />;
}
return (
<PaginatedDataList
contentError={credentialsError}
hasContentLoading={isCredentialsLoading}
itemCount={count}
items={credentials}
onRowClick={row => selectedCredentialHelper.setValue(row)}
qsConfig={QS_CONFIG}
renderItem={credential => (
<CheckboxListItem
isSelected={selectedCredential?.value?.id === credential.id}
itemId={credential.id}
key={credential.id}
name={credential.name}
label={credential.name}
onSelect={() => selectedCredentialHelper.setValue(credential)}
onDeselect={() => selectedCredentialHelper.setValue(null)}
isRadio
/>
)}
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
showPageSizeOptions={false}
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true,
},
{
name: i18n._(t`Created By (Username)`),
key: 'created_by__username',
},
{
name: i18n._(t`Modified By (Username)`),
key: 'modified_by__username',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
]}
/>
);
}
export default withI18n()(CredentialsStep);

View File

@ -0,0 +1,159 @@
import React, { useCallback, useEffect } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
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';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px;
`;
const TestButton = styled(Button)`
margin-top: 20px;
`;
function MetadataStep({ i18n }) {
const form = useFormikContext();
const [selectedCredential] = useField('credential');
const [inputValues] = useField('inputs');
const {
result: fields,
error,
isLoading,
request: fetchMetadataOptions,
} = useRequest(
useCallback(async () => {
const {
data: {
inputs: { required, metadata },
},
} = await CredentialTypesAPI.readDetail(
selectedCredential.value.credential_type ||
selectedCredential.value.credential_type_id
);
metadata.forEach(field => {
if (inputValues.value[field.id]) {
form.initialValues.inputs[field.id] = inputValues.value[field.id];
} else {
if (field.type === 'string' && field.choices) {
form.initialValues.inputs[field.id] =
field.default || field.choices[0];
} else {
form.initialValues.inputs[field.id] = '';
}
}
if (required && required.includes(field.id)) {
field.required = true;
}
});
return metadata;
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []),
[]
);
useEffect(() => {
fetchMetadataOptions();
}, [fetchMetadataOptions]);
const testMetadata = () => {
alert('not implemented');
};
if (isLoading) {
return <ContentLoading />;
}
if (error) {
return <ContentError error={error} />;
}
return (
<>
{fields.length > 0 && (
<Form>
<FormFullWidthLayout>
{fields.map(field => {
if (field.type === 'string') {
if (field.choices) {
return (
<FormGroup
key={field.id}
fieldId={`credential-${field.id}`}
label={field.label}
isRequired={field.required}
>
{field.help_text && (
<Tooltip content={field.help_text} position="right">
<QuestionCircleIcon />
</Tooltip>
)}
<AnsibleSelect
name={`inputs.${field.id}`}
value={form.values.inputs[field.id]}
id={`credential-${field.id}`}
data={field.choices.map(choice => {
return {
value: choice,
key: choice,
label: choice,
};
})}
onChange={(event, value) => {
form.setFieldValue(`inputs.${field.id}`, value);
}}
validate={field.required ? required(null, i18n) : null}
/>
</FormGroup>
);
}
return (
<FormField
key={field.id}
id={`credential-${field.id}`}
label={field.label}
tooltip={field.help_text}
name={`inputs.${field.id}`}
type={field.multiline ? 'textarea' : 'text'}
isRequired={field.required}
validate={field.required ? required(null, i18n) : null}
/>
);
}
return null;
})}
</FormFullWidthLayout>
</Form>
)}
<Tooltip
content={i18n._(
t`Click this button to verify connection to the secret management system using the selected credential and specified inputs.`
)}
position="right"
>
<TestButton
variant="primary"
type="submit"
onClick={() => testMetadata()}
>
{i18n._(t`Test`)}
</TestButton>
</Tooltip>
</>
);
}
export default withI18n()(MetadataStep);

View File

@ -0,0 +1,3 @@
export { default as CredentialPluginPrompt } from './CredentialPluginPrompt';
export { default as CredentialsStep } from './CredentialsStep';
export { default as MetadataStep } from './MetadataStep';

View File

@ -0,0 +1,54 @@
import React from 'react';
import { withI18n } from '@lingui/react';
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';
const SelectedCredential = styled.div`
display: flex;
justify-content: space-between;
margin-top: 10px;
background-color: white;
border-bottom-color: var(--pf-global--BorderColor--200);
`;
const SpacedCredentialChip = styled(CredentialChip)`
margin: 5px 8px;
`;
function CredentialPluginSelected({
i18n,
credential,
onEditPlugin,
onClearPlugin,
}) {
return (
<>
<p>
<Trans>
This field will be retrieved from an external secret management system
using the following credential:
</Trans>
</p>
<SelectedCredential>
<SpacedCredentialChip onClick={onClearPlugin} credential={credential} />
<Tooltip
content={i18n._(t`Edit Credential Plugin Configuration`)}
position="top"
>
<Button
aria-label={i18n._(t`Edit Credential Plugin Configuration`)}
onClick={onEditPlugin}
variant={ButtonVariant.control}
>
<KeyIcon />
</Button>
</Tooltip>
</SelectedCredential>
</>
);
}
export default withI18n()(CredentialPluginSelected);

View File

@ -0,0 +1,2 @@
export { default as CredentialPluginSelected } from './CredentialPluginSelected';
export { default as CredentialPluginField } from './CredentialPluginField';

View File

@ -2,13 +2,18 @@ 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';
import FormField from '../../../../components/FormField';
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);
@ -91,30 +96,38 @@ const GoogleComputeEngineSubForm = ({ i18n }) => {
}}
/>
</FormGroup>
<FormField
<CredentialPluginField
id="credential-username"
label={i18n._(t`Service account email address`)}
name="inputs.username"
type="email"
validate={required(null, i18n)}
isRequired
/>
<FormField
>
<TextInput id="credential-username" />
</CredentialPluginField>
<CredentialPluginField
id="credential-project"
label={i18n._(t`Project`)}
name="inputs.project"
type="text"
/>
>
<TextInput id="credential-project" />
</CredentialPluginField>
<FormFullWidthLayout>
<FormField
<CredentialPluginField
id="credential-sshKeyData"
label={i18n._(t`RSA private key`)}
name="inputs.ssh_key_data"
type="textarea"
rows={6}
validate={required(null, i18n)}
isRequired
/>
>
<TextArea
id="credential-sshKeyData"
rows={6}
resizeOrientation="vertical"
/>
</CredentialPluginField>
</FormFullWidthLayout>
</FormColumnLayout>
);

View File

@ -1,39 +1,50 @@
import React from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import FormField, { PasswordField } from '../../../../components/FormField';
import { TextArea, TextInput } from '@patternfly/react-core';
import { CredentialPluginField } from '../CredentialPlugins';
import { PasswordInput } from '../../../../components/FormField';
export const UsernameFormField = withI18n()(({ i18n }) => (
<FormField
id="credentual-username"
<CredentialPluginField
id="credential-username"
label={i18n._(t`Username`)}
name="inputs.username"
type="text"
/>
>
<TextInput id="credential-username" />
</CredentialPluginField>
));
export const PasswordFormField = withI18n()(({ i18n }) => (
<PasswordField
<CredentialPluginField
id="credential-password"
label={i18n._(t`Password`)}
name="inputs.password"
/>
>
<PasswordInput id="credential-password" />
</CredentialPluginField>
));
export const SSHKeyDataField = withI18n()(({ i18n }) => (
<FormField
<CredentialPluginField
id="credential-sshKeyData"
label={i18n._(t`SSH Private Key`)}
name="inputs.ssh_key_data"
type="textarea"
rows={6}
/>
>
<TextArea
id="credential-sshKeyData"
rows={6}
resizeOrientation="vertical"
/>
</CredentialPluginField>
));
export const SSHKeyUnlockField = withI18n()(({ i18n }) => (
<PasswordField
<CredentialPluginField
id="credential-sshKeyUnlock"
label={i18n._(t`Private Key Passphrase`)}
name="inputs.ssh_key_unlock"
/>
>
<PasswordInput id="credential-password" />
</CredentialPluginField>
));