mirror of
https://github.com/ansible/awx.git
synced 2026-01-15 20:00:43 -03:30
Adds basic credential plugin support to relevant fields in the static credential forms.
This commit is contained in:
parent
9d42b8f0f2
commit
4b95297bd4
@ -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,
|
||||
};
|
||||
|
||||
10
awx/ui_next/src/api/models/CredentialInputSources.js
Normal file
10
awx/ui_next/src/api/models/CredentialInputSources.js
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
71
awx/ui_next/src/components/FormField/PasswordInput.jsx
Normal file
71
awx/ui_next/src/components/FormField/PasswordInput.jsx
Normal 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);
|
||||
@ -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';
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -245,6 +245,7 @@ CredentialTypesAPI.read.mockResolvedValue({
|
||||
});
|
||||
|
||||
CredentialsAPI.update.mockResolvedValue({ data: { id: 3 } });
|
||||
CredentialsAPI.readInputSources.mockResolvedValue({ data: { results: [] } });
|
||||
|
||||
describe('<CredentialEdit />', () => {
|
||||
let wrapper;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -0,0 +1,3 @@
|
||||
export { default as CredentialPluginPrompt } from './CredentialPluginPrompt';
|
||||
export { default as CredentialsStep } from './CredentialsStep';
|
||||
export { default as MetadataStep } from './MetadataStep';
|
||||
@ -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);
|
||||
@ -0,0 +1,2 @@
|
||||
export { default as CredentialPluginSelected } from './CredentialPluginSelected';
|
||||
export { default as CredentialPluginField } from './CredentialPluginField';
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user