mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 01:17:37 -02:30
Adds basic credential plugin support to relevant fields in the static credential forms.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import AdHocCommands from './models/AdHocCommands';
|
import AdHocCommands from './models/AdHocCommands';
|
||||||
import Config from './models/Config';
|
import Config from './models/Config';
|
||||||
|
import CredentialInputSources from './models/CredentialInputSources';
|
||||||
import CredentialTypes from './models/CredentialTypes';
|
import CredentialTypes from './models/CredentialTypes';
|
||||||
import Credentials from './models/Credentials';
|
import Credentials from './models/Credentials';
|
||||||
import Groups from './models/Groups';
|
import Groups from './models/Groups';
|
||||||
@@ -14,8 +15,8 @@ import Labels from './models/Labels';
|
|||||||
import Me from './models/Me';
|
import Me from './models/Me';
|
||||||
import NotificationTemplates from './models/NotificationTemplates';
|
import NotificationTemplates from './models/NotificationTemplates';
|
||||||
import Organizations from './models/Organizations';
|
import Organizations from './models/Organizations';
|
||||||
import Projects from './models/Projects';
|
|
||||||
import ProjectUpdates from './models/ProjectUpdates';
|
import ProjectUpdates from './models/ProjectUpdates';
|
||||||
|
import Projects from './models/Projects';
|
||||||
import Root from './models/Root';
|
import Root from './models/Root';
|
||||||
import Schedules from './models/Schedules';
|
import Schedules from './models/Schedules';
|
||||||
import SystemJobs from './models/SystemJobs';
|
import SystemJobs from './models/SystemJobs';
|
||||||
@@ -24,14 +25,15 @@ import UnifiedJobTemplates from './models/UnifiedJobTemplates';
|
|||||||
import UnifiedJobs from './models/UnifiedJobs';
|
import UnifiedJobs from './models/UnifiedJobs';
|
||||||
import Users from './models/Users';
|
import Users from './models/Users';
|
||||||
import WorkflowApprovalTemplates from './models/WorkflowApprovalTemplates';
|
import WorkflowApprovalTemplates from './models/WorkflowApprovalTemplates';
|
||||||
import WorkflowJobs from './models/WorkflowJobs';
|
|
||||||
import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes';
|
import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes';
|
||||||
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
|
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
|
||||||
|
import WorkflowJobs from './models/WorkflowJobs';
|
||||||
|
|
||||||
const AdHocCommandsAPI = new AdHocCommands();
|
const AdHocCommandsAPI = new AdHocCommands();
|
||||||
const ConfigAPI = new Config();
|
const ConfigAPI = new Config();
|
||||||
const CredentialsAPI = new Credentials();
|
const CredentialInputSourcesAPI = new CredentialInputSources();
|
||||||
const CredentialTypesAPI = new CredentialTypes();
|
const CredentialTypesAPI = new CredentialTypes();
|
||||||
|
const CredentialsAPI = new Credentials();
|
||||||
const GroupsAPI = new Groups();
|
const GroupsAPI = new Groups();
|
||||||
const HostsAPI = new Hosts();
|
const HostsAPI = new Hosts();
|
||||||
const InstanceGroupsAPI = new InstanceGroups();
|
const InstanceGroupsAPI = new InstanceGroups();
|
||||||
@@ -44,8 +46,8 @@ const LabelsAPI = new Labels();
|
|||||||
const MeAPI = new Me();
|
const MeAPI = new Me();
|
||||||
const NotificationTemplatesAPI = new NotificationTemplates();
|
const NotificationTemplatesAPI = new NotificationTemplates();
|
||||||
const OrganizationsAPI = new Organizations();
|
const OrganizationsAPI = new Organizations();
|
||||||
const ProjectsAPI = new Projects();
|
|
||||||
const ProjectUpdatesAPI = new ProjectUpdates();
|
const ProjectUpdatesAPI = new ProjectUpdates();
|
||||||
|
const ProjectsAPI = new Projects();
|
||||||
const RootAPI = new Root();
|
const RootAPI = new Root();
|
||||||
const SchedulesAPI = new Schedules();
|
const SchedulesAPI = new Schedules();
|
||||||
const SystemJobsAPI = new SystemJobs();
|
const SystemJobsAPI = new SystemJobs();
|
||||||
@@ -54,15 +56,16 @@ const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
|
|||||||
const UnifiedJobsAPI = new UnifiedJobs();
|
const UnifiedJobsAPI = new UnifiedJobs();
|
||||||
const UsersAPI = new Users();
|
const UsersAPI = new Users();
|
||||||
const WorkflowApprovalTemplatesAPI = new WorkflowApprovalTemplates();
|
const WorkflowApprovalTemplatesAPI = new WorkflowApprovalTemplates();
|
||||||
const WorkflowJobsAPI = new WorkflowJobs();
|
|
||||||
const WorkflowJobTemplateNodesAPI = new WorkflowJobTemplateNodes();
|
const WorkflowJobTemplateNodesAPI = new WorkflowJobTemplateNodes();
|
||||||
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
|
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
|
||||||
|
const WorkflowJobsAPI = new WorkflowJobs();
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AdHocCommandsAPI,
|
AdHocCommandsAPI,
|
||||||
ConfigAPI,
|
ConfigAPI,
|
||||||
CredentialsAPI,
|
CredentialInputSourcesAPI,
|
||||||
CredentialTypesAPI,
|
CredentialTypesAPI,
|
||||||
|
CredentialsAPI,
|
||||||
GroupsAPI,
|
GroupsAPI,
|
||||||
HostsAPI,
|
HostsAPI,
|
||||||
InstanceGroupsAPI,
|
InstanceGroupsAPI,
|
||||||
@@ -75,8 +78,8 @@ export {
|
|||||||
MeAPI,
|
MeAPI,
|
||||||
NotificationTemplatesAPI,
|
NotificationTemplatesAPI,
|
||||||
OrganizationsAPI,
|
OrganizationsAPI,
|
||||||
ProjectsAPI,
|
|
||||||
ProjectUpdatesAPI,
|
ProjectUpdatesAPI,
|
||||||
|
ProjectsAPI,
|
||||||
RootAPI,
|
RootAPI,
|
||||||
SchedulesAPI,
|
SchedulesAPI,
|
||||||
SystemJobsAPI,
|
SystemJobsAPI,
|
||||||
@@ -85,7 +88,7 @@ export {
|
|||||||
UnifiedJobsAPI,
|
UnifiedJobsAPI,
|
||||||
UsersAPI,
|
UsersAPI,
|
||||||
WorkflowApprovalTemplatesAPI,
|
WorkflowApprovalTemplatesAPI,
|
||||||
WorkflowJobsAPI,
|
|
||||||
WorkflowJobTemplateNodesAPI,
|
WorkflowJobTemplateNodesAPI,
|
||||||
WorkflowJobTemplatesAPI,
|
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.baseUrl = '/api/v2/credentials/';
|
||||||
|
|
||||||
this.readAccessList = this.readAccessList.bind(this);
|
this.readAccessList = this.readAccessList.bind(this);
|
||||||
|
this.readInputSources = this.readInputSources.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
readAccessList(id, params) {
|
readAccessList(id, params) {
|
||||||
@@ -13,6 +14,12 @@ class Credentials extends Base {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readInputSources(id, params) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/input_sources/`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Credentials;
|
export default Credentials;
|
||||||
|
|||||||
@@ -1,29 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import {
|
import { FormGroup, InputGroup } from '@patternfly/react-core';
|
||||||
Button,
|
import { PasswordInput } from '.';
|
||||||
ButtonVariant,
|
|
||||||
FormGroup,
|
|
||||||
InputGroup,
|
|
||||||
TextInput,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
function PasswordField(props) {
|
function PasswordField(props) {
|
||||||
const { id, name, label, validate, isRequired, isDisabled, i18n } = props;
|
const { id, name, label, validate, isRequired } = props;
|
||||||
const [inputType, setInputType] = useState('password');
|
const [, meta] = useField({ name, validate });
|
||||||
const [field, meta] = useField({ name, validate });
|
|
||||||
|
|
||||||
const isValid = !(meta.touched && meta.error);
|
const isValid = !(meta.touched && meta.error);
|
||||||
|
|
||||||
const handlePasswordToggle = () => {
|
|
||||||
setInputType(inputType === 'text' ? 'password' : 'text');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId={id}
|
fieldId={id}
|
||||||
@@ -33,32 +18,7 @@ function PasswordField(props) {
|
|||||||
label={label}
|
label={label}
|
||||||
>
|
>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Tooltip
|
<PasswordInput {...props} />
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
@@ -79,4 +39,4 @@ PasswordField.defaultProps = {
|
|||||||
isDisabled: false,
|
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 CheckboxField } from './CheckboxField';
|
||||||
export { default as FieldTooltip } from './FieldTooltip';
|
export { default as FieldTooltip } from './FieldTooltip';
|
||||||
export { default as PasswordField } from './PasswordField';
|
export { default as PasswordField } from './PasswordField';
|
||||||
|
export { default as PasswordInput } from './PasswordInput';
|
||||||
export { default as FormSubmitError } from './FormSubmitError';
|
export { default as FormSubmitError } from './FormSubmitError';
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { CardBody } from '../../../components/Card';
|
|||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
import ContentLoading from '../../../components/ContentLoading';
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
|
|
||||||
import { CredentialTypesAPI, CredentialsAPI } from '../../../api';
|
import {
|
||||||
|
CredentialInputSourcesAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
|
CredentialsAPI,
|
||||||
|
} from '../../../api';
|
||||||
import CredentialForm from '../shared/CredentialForm';
|
import CredentialForm from '../shared/CredentialForm';
|
||||||
|
|
||||||
function CredentialAdd({ me }) {
|
function CredentialAdd({ me }) {
|
||||||
@@ -38,16 +42,41 @@ function CredentialAdd({ me }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
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);
|
setFormSubmitError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { id: credentialId },
|
data: { id: credentialId },
|
||||||
} = await CredentialsAPI.create({
|
} = await CredentialsAPI.create({
|
||||||
user: (me && me.id) || null,
|
user: (me && me.id) || null,
|
||||||
organization: (organization && organization.id) || null,
|
organization: (organization && organization.id) || null,
|
||||||
|
inputs: inputs || {},
|
||||||
...remainingValues,
|
...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`;
|
const url = `/credentials/${credentialId}/details`;
|
||||||
history.push(`${url}`);
|
history.push(`${url}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { useHistory } from 'react-router-dom';
|
|||||||
import { object } from 'prop-types';
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
import {
|
||||||
|
CredentialsAPI,
|
||||||
|
CredentialInputSourcesAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
|
} from '../../../api';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
import ContentLoading from '../../../components/ContentLoading';
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
import CredentialForm from '../shared/CredentialForm';
|
import CredentialForm from '../shared/CredentialForm';
|
||||||
@@ -12,18 +16,32 @@ function CredentialEdit({ credential, me }) {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [credentialTypes, setCredentialTypes] = useState(null);
|
const [credentialTypes, setCredentialTypes] = useState(null);
|
||||||
|
const [inputSources, setInputSources] = useState(null);
|
||||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const [
|
||||||
data: { results: loadedCredentialTypes },
|
{
|
||||||
} = await CredentialTypesAPI.read({
|
data: { results: loadedCredentialTypes },
|
||||||
or__namespace: ['gce', 'scm', 'ssh'],
|
},
|
||||||
});
|
{
|
||||||
|
data: { results: loadedInputSources },
|
||||||
|
},
|
||||||
|
] = await Promise.all([
|
||||||
|
CredentialTypesAPI.read({
|
||||||
|
or__namespace: ['gce', 'scm', 'ssh'],
|
||||||
|
}),
|
||||||
|
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
|
||||||
|
]);
|
||||||
setCredentialTypes(loadedCredentialTypes);
|
setCredentialTypes(loadedCredentialTypes);
|
||||||
|
const inputSourcesMap = {};
|
||||||
|
loadedInputSources.forEach(inputSource => {
|
||||||
|
inputSourcesMap[inputSource.input_field_name] = inputSource;
|
||||||
|
});
|
||||||
|
setInputSources(inputSourcesMap);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -31,7 +49,7 @@ function CredentialEdit({ credential, me }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, [credential.id]);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
const url = `/credentials/${credential.id}/details`;
|
const url = `/credentials/${credential.id}/details`;
|
||||||
@@ -39,20 +57,62 @@ function CredentialEdit({ credential, me }) {
|
|||||||
history.push(`${url}`);
|
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 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);
|
setFormSubmitError(null);
|
||||||
try {
|
try {
|
||||||
const {
|
await Promise.all([
|
||||||
data: { id: credentialId },
|
CredentialsAPI.update(credential.id, {
|
||||||
} = await CredentialsAPI.update(credential.id, {
|
user: (me && me.id) || null,
|
||||||
user: (me && me.id) || null,
|
organization: (organization && organization.id) || null,
|
||||||
organization: (organization && organization.id) || null,
|
inputs: inputs || {},
|
||||||
...remainingValues,
|
...remainingValues,
|
||||||
});
|
}),
|
||||||
const url = `/credentials/${credentialId}/details`;
|
...destroyInputSources(pluginInputs),
|
||||||
|
]);
|
||||||
|
await Promise.all(createAndUpdateInputSources(pluginInputs));
|
||||||
|
const url = `/credentials/${credential.id}/details`;
|
||||||
history.push(`${url}`);
|
history.push(`${url}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
setFormSubmitError(err);
|
setFormSubmitError(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -72,6 +132,7 @@ function CredentialEdit({ credential, me }) {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
credential={credential}
|
credential={credential}
|
||||||
credentialTypes={credentialTypes}
|
credentialTypes={credentialTypes}
|
||||||
|
inputSources={inputSources}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ CredentialTypesAPI.read.mockResolvedValue({
|
|||||||
});
|
});
|
||||||
|
|
||||||
CredentialsAPI.update.mockResolvedValue({ data: { id: 3 } });
|
CredentialsAPI.update.mockResolvedValue({ data: { id: 3 } });
|
||||||
|
CredentialsAPI.readInputSources.mockResolvedValue({ data: { results: [] } });
|
||||||
|
|
||||||
describe('<CredentialEdit />', () => {
|
describe('<CredentialEdit />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { Formik, useField } from 'formik';
|
import { Formik, useField } from 'formik';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { func, shape } from 'prop-types';
|
import { arrayOf, func, object, shape } from 'prop-types';
|
||||||
import { Form, FormGroup, Title } from '@patternfly/react-core';
|
import { Form, FormGroup, Title } from '@patternfly/react-core';
|
||||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
@@ -124,6 +124,7 @@ function CredentialFormFields({
|
|||||||
function CredentialForm({
|
function CredentialForm({
|
||||||
credential = {},
|
credential = {},
|
||||||
credentialTypes,
|
credentialTypes,
|
||||||
|
inputSources,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitError,
|
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)
|
const scmCredentialTypeId = Object.keys(credentialTypes)
|
||||||
.filter(key => credentialTypes[key].namespace === 'scm')
|
.filter(key => credentialTypes[key].namespace === 'scm')
|
||||||
.map(key => credentialTypes[key].id)[0];
|
.map(key => credentialTypes[key].id)[0];
|
||||||
@@ -232,10 +240,12 @@ CredentialForm.proptype = {
|
|||||||
handleSubmit: func.isRequired,
|
handleSubmit: func.isRequired,
|
||||||
handleCancel: func.isRequired,
|
handleCancel: func.isRequired,
|
||||||
credential: shape({}),
|
credential: shape({}),
|
||||||
|
inputSources: arrayOf(object),
|
||||||
};
|
};
|
||||||
|
|
||||||
CredentialForm.defaultProps = {
|
CredentialForm.defaultProps = {
|
||||||
credential: {},
|
credential: {},
|
||||||
|
inputSources: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(CredentialForm);
|
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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { FileUpload, FormGroup } from '@patternfly/react-core';
|
import {
|
||||||
import FormField from '../../../../components/FormField';
|
FileUpload,
|
||||||
|
FormGroup,
|
||||||
|
TextArea,
|
||||||
|
TextInput,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
FormColumnLayout,
|
FormColumnLayout,
|
||||||
FormFullWidthLayout,
|
FormFullWidthLayout,
|
||||||
} from '../../../../components/FormLayout';
|
} from '../../../../components/FormLayout';
|
||||||
import { required } from '../../../../util/validators';
|
import { required } from '../../../../util/validators';
|
||||||
|
import { CredentialPluginField } from '../CredentialPlugins';
|
||||||
|
|
||||||
const GoogleComputeEngineSubForm = ({ i18n }) => {
|
const GoogleComputeEngineSubForm = ({ i18n }) => {
|
||||||
const [fileError, setFileError] = useState(null);
|
const [fileError, setFileError] = useState(null);
|
||||||
@@ -91,30 +96,38 @@ const GoogleComputeEngineSubForm = ({ i18n }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormField
|
<CredentialPluginField
|
||||||
id="credential-username"
|
id="credential-username"
|
||||||
label={i18n._(t`Service account email address`)}
|
label={i18n._(t`Service account email address`)}
|
||||||
name="inputs.username"
|
name="inputs.username"
|
||||||
type="email"
|
type="email"
|
||||||
validate={required(null, i18n)}
|
validate={required(null, i18n)}
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
>
|
||||||
<FormField
|
<TextInput id="credential-username" />
|
||||||
|
</CredentialPluginField>
|
||||||
|
<CredentialPluginField
|
||||||
id="credential-project"
|
id="credential-project"
|
||||||
label={i18n._(t`Project`)}
|
label={i18n._(t`Project`)}
|
||||||
name="inputs.project"
|
name="inputs.project"
|
||||||
type="text"
|
>
|
||||||
/>
|
<TextInput id="credential-project" />
|
||||||
|
</CredentialPluginField>
|
||||||
<FormFullWidthLayout>
|
<FormFullWidthLayout>
|
||||||
<FormField
|
<CredentialPluginField
|
||||||
id="credential-sshKeyData"
|
id="credential-sshKeyData"
|
||||||
label={i18n._(t`RSA private key`)}
|
label={i18n._(t`RSA private key`)}
|
||||||
name="inputs.ssh_key_data"
|
name="inputs.ssh_key_data"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
rows={6}
|
|
||||||
validate={required(null, i18n)}
|
validate={required(null, i18n)}
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
>
|
||||||
|
<TextArea
|
||||||
|
id="credential-sshKeyData"
|
||||||
|
rows={6}
|
||||||
|
resizeOrientation="vertical"
|
||||||
|
/>
|
||||||
|
</CredentialPluginField>
|
||||||
</FormFullWidthLayout>
|
</FormFullWidthLayout>
|
||||||
</FormColumnLayout>
|
</FormColumnLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,39 +1,50 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
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 }) => (
|
export const UsernameFormField = withI18n()(({ i18n }) => (
|
||||||
<FormField
|
<CredentialPluginField
|
||||||
id="credentual-username"
|
id="credential-username"
|
||||||
label={i18n._(t`Username`)}
|
label={i18n._(t`Username`)}
|
||||||
name="inputs.username"
|
name="inputs.username"
|
||||||
type="text"
|
>
|
||||||
/>
|
<TextInput id="credential-username" />
|
||||||
|
</CredentialPluginField>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const PasswordFormField = withI18n()(({ i18n }) => (
|
export const PasswordFormField = withI18n()(({ i18n }) => (
|
||||||
<PasswordField
|
<CredentialPluginField
|
||||||
id="credential-password"
|
id="credential-password"
|
||||||
label={i18n._(t`Password`)}
|
label={i18n._(t`Password`)}
|
||||||
name="inputs.password"
|
name="inputs.password"
|
||||||
/>
|
>
|
||||||
|
<PasswordInput id="credential-password" />
|
||||||
|
</CredentialPluginField>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const SSHKeyDataField = withI18n()(({ i18n }) => (
|
export const SSHKeyDataField = withI18n()(({ i18n }) => (
|
||||||
<FormField
|
<CredentialPluginField
|
||||||
id="credential-sshKeyData"
|
id="credential-sshKeyData"
|
||||||
label={i18n._(t`SSH Private Key`)}
|
label={i18n._(t`SSH Private Key`)}
|
||||||
name="inputs.ssh_key_data"
|
name="inputs.ssh_key_data"
|
||||||
type="textarea"
|
>
|
||||||
rows={6}
|
<TextArea
|
||||||
/>
|
id="credential-sshKeyData"
|
||||||
|
rows={6}
|
||||||
|
resizeOrientation="vertical"
|
||||||
|
/>
|
||||||
|
</CredentialPluginField>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const SSHKeyUnlockField = withI18n()(({ i18n }) => (
|
export const SSHKeyUnlockField = withI18n()(({ i18n }) => (
|
||||||
<PasswordField
|
<CredentialPluginField
|
||||||
id="credential-sshKeyUnlock"
|
id="credential-sshKeyUnlock"
|
||||||
label={i18n._(t`Private Key Passphrase`)}
|
label={i18n._(t`Private Key Passphrase`)}
|
||||||
name="inputs.ssh_key_unlock"
|
name="inputs.ssh_key_unlock"
|
||||||
/>
|
>
|
||||||
|
<PasswordInput id="credential-password" />
|
||||||
|
</CredentialPluginField>
|
||||||
));
|
));
|
||||||
|
|||||||
Reference in New Issue
Block a user