mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
Prevent users from selecting credentials that prompt for passwords on workflow nodes and schedules
This commit is contained in:
parent
3878d8f7d8
commit
467536f93f
@ -36,6 +36,7 @@ function LaunchButton({ resource, i18n, children, history }) {
|
||||
const [showLaunchPrompt, setShowLaunchPrompt] = useState(false);
|
||||
const [launchConfig, setLaunchConfig] = useState(null);
|
||||
const [surveyConfig, setSurveyConfig] = useState(null);
|
||||
const [resourceCredentials, setResourceCredentials] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const handleLaunch = async () => {
|
||||
const readLaunch =
|
||||
@ -56,6 +57,17 @@ function LaunchButton({ resource, i18n, children, history }) {
|
||||
setSurveyConfig(data);
|
||||
}
|
||||
|
||||
if (
|
||||
launch.ask_credential_on_launch &&
|
||||
resource.type === 'workflow_job_template'
|
||||
) {
|
||||
const {
|
||||
data: { results: jobTemplateCredentials },
|
||||
} = await JobTemplatesAPI.readCredentials(resource.id);
|
||||
|
||||
setResourceCredentials(jobTemplateCredentials);
|
||||
}
|
||||
|
||||
if (canLaunchWithoutPrompt(launch)) {
|
||||
launchWithParams({});
|
||||
} else {
|
||||
@ -161,6 +173,7 @@ function LaunchButton({ resource, i18n, children, history }) {
|
||||
resource={resource}
|
||||
onLaunch={launchWithParams}
|
||||
onCancel={() => setShowLaunchPrompt(false)}
|
||||
resourceDefaultCredentials={resourceCredentials}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@ -18,6 +18,7 @@ function PromptModalForm({
|
||||
onSubmit,
|
||||
resource,
|
||||
surveyConfig,
|
||||
resourceDefaultCredentials,
|
||||
}) {
|
||||
const { setFieldTouched, values } = useFormikContext();
|
||||
|
||||
@ -28,7 +29,13 @@ function PromptModalForm({
|
||||
visitStep,
|
||||
visitAllSteps,
|
||||
contentError,
|
||||
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
||||
} = useLaunchSteps(
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resource,
|
||||
i18n,
|
||||
resourceDefaultCredentials
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const postValues = {};
|
||||
@ -122,6 +129,7 @@ function LaunchPrompt({
|
||||
onLaunch,
|
||||
resource = {},
|
||||
surveyConfig,
|
||||
resourceDefaultCredentials = [],
|
||||
}) {
|
||||
return (
|
||||
<Formik initialValues={{}} onSubmit={values => onLaunch(values)}>
|
||||
@ -132,6 +140,7 @@ function LaunchPrompt({
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resource={resource}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
|
||||
@ -87,7 +87,9 @@ describe('LaunchPrompt', () => {
|
||||
credentials: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'cred that prompts',
|
||||
passwords_needed: ['ssh_password'],
|
||||
credential_type: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -122,6 +124,16 @@ describe('LaunchPrompt', () => {
|
||||
},
|
||||
],
|
||||
}}
|
||||
resourceDefaultCredentials={[
|
||||
{
|
||||
id: 5,
|
||||
name: 'cred that prompts',
|
||||
credential_type: 1,
|
||||
inputs: {
|
||||
password: 'ASK',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@ -4,7 +4,8 @@ import { useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { ToolbarItem } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Alert, ToolbarItem } from '@patternfly/react-core';
|
||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
||||
import AnsibleSelect from '../../AnsibleSelect';
|
||||
import OptionsList from '../../OptionsList';
|
||||
@ -13,7 +14,11 @@ import CredentialChip from '../../CredentialChip';
|
||||
import ContentError from '../../ContentError';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { required } from '../../../util/validators';
|
||||
import credentialsValidator from './credentialsValidator';
|
||||
|
||||
const CredentialErrorAlert = styled(Alert)`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const QS_CONFIG = getQSConfig('credential', {
|
||||
page: 1,
|
||||
@ -21,10 +26,21 @@ const QS_CONFIG = getQSConfig('credential', {
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function CredentialsStep({ i18n }) {
|
||||
const [field, , helpers] = useField({
|
||||
function CredentialsStep({
|
||||
i18n,
|
||||
allowCredentialsWithPasswords,
|
||||
defaultCredentials = [],
|
||||
}) {
|
||||
const [field, meta, helpers] = useField({
|
||||
name: 'credentials',
|
||||
validate: required(null, i18n),
|
||||
validate: val => {
|
||||
return credentialsValidator(
|
||||
i18n,
|
||||
defaultCredentials,
|
||||
allowCredentialsWithPasswords,
|
||||
val
|
||||
);
|
||||
},
|
||||
});
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const history = useHistory();
|
||||
@ -87,6 +103,18 @@ function CredentialsStep({ i18n }) {
|
||||
fetchCredentials();
|
||||
}, [fetchCredentials]);
|
||||
|
||||
useEffect(() => {
|
||||
helpers.setError(
|
||||
credentialsValidator(
|
||||
i18n,
|
||||
defaultCredentials,
|
||||
allowCredentialsWithPasswords,
|
||||
field.value
|
||||
)
|
||||
);
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, []);
|
||||
|
||||
if (isTypesLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
@ -97,17 +125,23 @@ function CredentialsStep({ i18n }) {
|
||||
|
||||
const isVault = selectedType?.kind === 'vault';
|
||||
|
||||
const renderChip = ({ item, removeItem, canDelete }) => (
|
||||
<CredentialChip
|
||||
key={item.id}
|
||||
onClick={() => removeItem(item)}
|
||||
isReadOnly={!canDelete}
|
||||
credential={item}
|
||||
/>
|
||||
);
|
||||
const renderChip = ({ item, removeItem, canDelete }) => {
|
||||
return (
|
||||
<CredentialChip
|
||||
id={`credential-chip-${item.id}`}
|
||||
key={item.id}
|
||||
onClick={() => removeItem(item)}
|
||||
isReadOnly={!canDelete}
|
||||
credential={item}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{meta.error && (
|
||||
<CredentialErrorAlert variant="danger" isInline title={meta.error} />
|
||||
)}
|
||||
{types && types.length > 0 && (
|
||||
<ToolbarItem css=" display: flex; align-items: center;">
|
||||
<div css="flex: 0 0 25%; margin-right: 32px">
|
||||
@ -130,57 +164,56 @@ function CredentialsStep({ i18n }) {
|
||||
/>
|
||||
</ToolbarItem>
|
||||
)}
|
||||
{!isCredentialsLoading && (
|
||||
<OptionsList
|
||||
value={field.value || []}
|
||||
options={credentials}
|
||||
optionCount={count}
|
||||
searchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username__icontains',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username__icontains',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
searchableKeys={searchableKeys}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
multiple={isVault}
|
||||
header={i18n._(t`Credentials`)}
|
||||
name="credentials"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly={false}
|
||||
selectItem={item => {
|
||||
const hasSameVaultID = val =>
|
||||
val?.inputs?.vault_id !== undefined &&
|
||||
val?.inputs?.vault_id === item?.inputs?.vault_id;
|
||||
const hasSameCredentialType = val =>
|
||||
val.credential_type === item.credential_type;
|
||||
const newItems = field.value.filter(i =>
|
||||
isVault ? !hasSameVaultID(i) : !hasSameCredentialType(i)
|
||||
);
|
||||
newItems.push(item);
|
||||
helpers.setValue(newItems);
|
||||
}}
|
||||
deselectItem={item => {
|
||||
helpers.setValue(field.value.filter(i => i.id !== item.id));
|
||||
}}
|
||||
renderItemChip={renderChip}
|
||||
/>
|
||||
)}
|
||||
<OptionsList
|
||||
isLoading={isCredentialsLoading}
|
||||
value={field.value || []}
|
||||
options={credentials}
|
||||
optionCount={count}
|
||||
searchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username__icontains',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username__icontains',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
searchableKeys={searchableKeys}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
multiple={isVault}
|
||||
header={i18n._(t`Credentials`)}
|
||||
name="credentials"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly={false}
|
||||
selectItem={item => {
|
||||
const hasSameVaultID = val =>
|
||||
val?.inputs?.vault_id !== undefined &&
|
||||
val?.inputs?.vault_id === item?.inputs?.vault_id;
|
||||
const hasSameCredentialType = val =>
|
||||
val.credential_type === item.credential_type;
|
||||
const newItems = field.value.filter(i =>
|
||||
isVault ? !hasSameVaultID(i) : !hasSameCredentialType(i)
|
||||
);
|
||||
newItems.push(item);
|
||||
helpers.setValue(newItems);
|
||||
}}
|
||||
deselectItem={item => {
|
||||
helpers.setValue(field.value.filter(i => i.id !== item.id));
|
||||
}}
|
||||
renderItemChip={renderChip}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,17 +9,95 @@ jest.mock('../../../api/models/CredentialTypes');
|
||||
jest.mock('../../../api/models/Credentials');
|
||||
|
||||
const types = [
|
||||
{ id: 1, kind: 'ssh', name: 'SSH' },
|
||||
{ id: 2, kind: 'cloud', name: 'Ansible Tower' },
|
||||
{ id: 3, kind: 'vault', name: 'Vault' },
|
||||
{ id: 1, kind: 'ssh', name: 'SSH', url: '/api/v2/credential_types/1/' },
|
||||
{ id: 3, kind: 'vault', name: 'Vault', url: '/api/v2/credential_types/3/' },
|
||||
{
|
||||
id: 5,
|
||||
name: 'Amazon Web Services',
|
||||
kind: 'cloud',
|
||||
url: '/api/v2/credential_types/5/',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Google Compute Engine',
|
||||
kind: 'cloud',
|
||||
url: '/api/v2/credential_types/9/',
|
||||
},
|
||||
];
|
||||
|
||||
const credentials = [
|
||||
{ id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' },
|
||||
{ id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' },
|
||||
{ id: 3, kind: 'Ansible', name: 'Cred 3', url: 'www.google.com' },
|
||||
{ id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' },
|
||||
{ id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
||||
{
|
||||
id: 1,
|
||||
kind: 'aws',
|
||||
name: 'Cred 1',
|
||||
credential_type: 5,
|
||||
url: '/api/v2/credentials/1/',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
kind: 'ssh',
|
||||
name: 'Cred 2',
|
||||
credential_type: 1,
|
||||
url: '/api/v2/credentials/2/',
|
||||
inputs: {
|
||||
password: 'ASK',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
kind: 'gce',
|
||||
name: 'Cred 3',
|
||||
credential_type: 9,
|
||||
url: '/api/v2/credentials/3/',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
kind: 'ssh',
|
||||
name: 'Cred 4',
|
||||
credential_type: 1,
|
||||
url: '/api/v2/credentials/4/',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
kind: 'ssh',
|
||||
name: 'Cred 5',
|
||||
credential_type: 1,
|
||||
url: '/api/v2/credentials/5/',
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
kind: 'vault',
|
||||
name: 'Cred 33',
|
||||
credential_type: 3,
|
||||
url: '/api/v2/credentials/33/',
|
||||
inputs: {
|
||||
vault_id: 'foo',
|
||||
},
|
||||
summary_fields: {
|
||||
credential_type: {
|
||||
name: 'Vault',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 34,
|
||||
kind: 'vault',
|
||||
name: 'Cred 34',
|
||||
credential_type: 3,
|
||||
url: '/api/v2/credentials/34/',
|
||||
inputs: {
|
||||
vault_id: 'bar',
|
||||
},
|
||||
summary_fields: {
|
||||
credential_type: {
|
||||
name: 'Vault',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('CredentialsStep', () => {
|
||||
@ -47,7 +125,7 @@ describe('CredentialsStep', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<CredentialsStep />
|
||||
<CredentialsStep allowCredentialsWithPasswords />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
@ -62,7 +140,7 @@ describe('CredentialsStep', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<CredentialsStep />
|
||||
<CredentialsStep allowCredentialsWithPasswords />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
@ -76,13 +154,173 @@ describe('CredentialsStep', () => {
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect').invoke('onChange')({}, 2);
|
||||
wrapper.find('AnsibleSelect').invoke('onChange')({}, 3);
|
||||
});
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||
credential_type: 2,
|
||||
credential_type: 3,
|
||||
order_by: 'name',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test("error should be shown when a credential that prompts for passwords is selected on a step that doesn't allow it", async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik
|
||||
initialValues={{
|
||||
credentials: [],
|
||||
}}
|
||||
>
|
||||
<CredentialsStep allowCredentialsWithPasswords={false} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-2"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').length).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Alert')
|
||||
.text()
|
||||
.includes('Cred 2')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('error should be toggled when default machine credential is removed and then replaced', async () => {
|
||||
let wrapper;
|
||||
const selectedCredentials = [
|
||||
{
|
||||
id: 5,
|
||||
kind: 'ssh',
|
||||
name: 'Cred 5',
|
||||
credential_type: 1,
|
||||
url: '/api/v2/credentials/5/',
|
||||
inputs: {},
|
||||
summary_fields: {
|
||||
credential_type: {
|
||||
name: 'Machine',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik
|
||||
initialValues={{
|
||||
credentials: selectedCredentials,
|
||||
}}
|
||||
>
|
||||
<CredentialsStep
|
||||
allowCredentialsWithPasswords={false}
|
||||
defaultCredentials={selectedCredentials}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').length).toBe(0);
|
||||
expect(wrapper.find('CredentialChip').length).toBe(1);
|
||||
await act(async () => {
|
||||
wrapper.find('button#remove_credential-chip-5').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').length).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Alert')
|
||||
.text()
|
||||
.includes('Machine')
|
||||
).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-5"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').length).toBe(0);
|
||||
});
|
||||
|
||||
test('error should be toggled when default vault credential is removed and then replaced', async () => {
|
||||
let wrapper;
|
||||
const selectedCredentials = [
|
||||
{
|
||||
id: 33,
|
||||
kind: 'vault',
|
||||
name: 'Cred 33',
|
||||
credential_type: 3,
|
||||
url: '/api/v2/credentials/33/',
|
||||
inputs: {
|
||||
vault_id: 'foo',
|
||||
},
|
||||
summary_fields: {
|
||||
credential_type: {
|
||||
name: 'Vault',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 34,
|
||||
kind: 'vault',
|
||||
name: 'Cred 34',
|
||||
credential_type: 3,
|
||||
url: '/api/v2/credentials/34/',
|
||||
inputs: {
|
||||
vault_id: 'bar',
|
||||
},
|
||||
summary_fields: {
|
||||
credential_type: {
|
||||
name: 'Vault',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik
|
||||
initialValues={{
|
||||
credentials: selectedCredentials,
|
||||
}}
|
||||
>
|
||||
<CredentialsStep
|
||||
allowCredentialsWithPasswords={false}
|
||||
defaultCredentials={selectedCredentials}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').length).toBe(0);
|
||||
expect(wrapper.find('CredentialChip').length).toBe(2);
|
||||
await act(async () => {
|
||||
wrapper.find('button#remove_credential-chip-33').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('CredentialChip').length).toBe(1);
|
||||
expect(wrapper.find('Alert').length).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Alert')
|
||||
.text()
|
||||
.includes('Vault | foo')
|
||||
).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect').invoke('onChange')({}, 3);
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-33"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import { useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
@ -11,6 +12,10 @@ import OptionsList from '../../OptionsList';
|
||||
import ContentLoading from '../../ContentLoading';
|
||||
import ContentError from '../../ContentError';
|
||||
|
||||
const InventoryErrorAlert = styled(Alert)`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const QS_CONFIG = getQSConfig('inventory', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
@ -68,6 +73,9 @@ function InventoryStep({ i18n, warningMessage = null }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{meta.touched && meta.error && (
|
||||
<InventoryErrorAlert variant="danger" isInline title={meta.error} />
|
||||
)}
|
||||
{warningMessage}
|
||||
<OptionsList
|
||||
value={field.value ? [field.value] : []}
|
||||
@ -103,9 +111,6 @@ function InventoryStep({ i18n, warningMessage = null }) {
|
||||
selectItem={helpers.setValue}
|
||||
deselectItem={() => field.onChange(null)}
|
||||
/>
|
||||
{meta.touched && meta.error && (
|
||||
<Alert variant="danger" isInline title={meta.error} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
const credentialPromptsForPassword = credential =>
|
||||
credential?.inputs?.password === 'ASK' ||
|
||||
credential?.inputs?.ssh_key_unlock === 'ASK' ||
|
||||
credential?.inputs?.become_password === 'ASK' ||
|
||||
credential?.inputs?.vault_password === 'ASK';
|
||||
|
||||
export default function credentialsValidator(
|
||||
i18n,
|
||||
defaultCredentials = [],
|
||||
allowCredentialsWithPasswords,
|
||||
selectedCredentials
|
||||
) {
|
||||
if (defaultCredentials.length > 0 && selectedCredentials) {
|
||||
const missingCredentialTypes = [];
|
||||
defaultCredentials.forEach(defaultCredential => {
|
||||
if (
|
||||
!selectedCredentials.find(selectedCredential => {
|
||||
return (
|
||||
(selectedCredential.credential_type ===
|
||||
defaultCredential.credential_type &&
|
||||
!selectedCredential.inputs.vault_id &&
|
||||
!defaultCredential.inputs.vault_id) ||
|
||||
(selectedCredential.inputs.vault_id &&
|
||||
defaultCredential.inputs.vault_id &&
|
||||
selectedCredential.inputs.vault_id ===
|
||||
defaultCredential.inputs.vault_id)
|
||||
);
|
||||
})
|
||||
) {
|
||||
missingCredentialTypes.push(
|
||||
defaultCredential.inputs.vault_id
|
||||
? `${defaultCredential.summary_fields.credential_type.name} | ${defaultCredential.inputs.vault_id}`
|
||||
: defaultCredential.summary_fields.credential_type.name
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (missingCredentialTypes.length > 0) {
|
||||
return i18n._(
|
||||
t`Job Template default credentials must be replaced with one of the same type. Please select a credential for the following types in order to proceed: ${missingCredentialTypes.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowCredentialsWithPasswords && selectedCredentials) {
|
||||
const credentialsThatPrompt = [];
|
||||
selectedCredentials.forEach(selectedCredential => {
|
||||
if (credentialPromptsForPassword(selectedCredential)) {
|
||||
credentialsThatPrompt.push(selectedCredential.name);
|
||||
}
|
||||
});
|
||||
if (credentialsThatPrompt.length > 0) {
|
||||
return i18n._(
|
||||
t`Credentials that require passwords on launch are not permitted. Please remove or replace the following credentials with a credential of the same type in order to proceed: ${credentialsThatPrompt.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -1,25 +1,59 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import CredentialsStep from './CredentialsStep';
|
||||
import StepName from './StepName';
|
||||
import credentialsValidator from './credentialsValidator';
|
||||
|
||||
const STEP_ID = 'credentials';
|
||||
|
||||
export default function useCredentialsStep(launchConfig, resource, i18n) {
|
||||
export default function useCredentialsStep(
|
||||
launchConfig,
|
||||
resource,
|
||||
resourceDefaultCredentials,
|
||||
i18n,
|
||||
allowCredentialsWithPasswords = false
|
||||
) {
|
||||
const [field, meta, helpers] = useField('credentials');
|
||||
const formError =
|
||||
!resource || resource?.type === 'workflow_job_template'
|
||||
? false
|
||||
: meta.error;
|
||||
return {
|
||||
step: getStep(launchConfig, i18n),
|
||||
initialValues: getInitialValues(launchConfig, resource),
|
||||
step: getStep(
|
||||
launchConfig,
|
||||
i18n,
|
||||
allowCredentialsWithPasswords,
|
||||
formError,
|
||||
resourceDefaultCredentials
|
||||
),
|
||||
initialValues: getInitialValues(launchConfig, resourceDefaultCredentials),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
hasError: false,
|
||||
hasError: launchConfig.ask_credential_on_launch && formError,
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('credentials', true, false);
|
||||
},
|
||||
validate: () => {},
|
||||
validate: () => {
|
||||
helpers.setError(
|
||||
credentialsValidator(
|
||||
i18n,
|
||||
resourceDefaultCredentials,
|
||||
allowCredentialsWithPasswords,
|
||||
field.value
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getStep(launchConfig, i18n) {
|
||||
function getStep(
|
||||
launchConfig,
|
||||
i18n,
|
||||
allowCredentialsWithPasswords,
|
||||
formError,
|
||||
resourceDefaultCredentials
|
||||
) {
|
||||
if (!launchConfig.ask_credential_on_launch) {
|
||||
return null;
|
||||
}
|
||||
@ -27,21 +61,27 @@ function getStep(launchConfig, i18n) {
|
||||
id: STEP_ID,
|
||||
key: 4,
|
||||
name: (
|
||||
<StepName hasErrors={false} id="credentials-step">
|
||||
<StepName hasErrors={formError} id="credentials-step">
|
||||
{i18n._(t`Credentials`)}
|
||||
</StepName>
|
||||
),
|
||||
component: <CredentialsStep i18n={i18n} />,
|
||||
component: (
|
||||
<CredentialsStep
|
||||
i18n={i18n}
|
||||
allowCredentialsWithPasswords={allowCredentialsWithPasswords}
|
||||
defaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
),
|
||||
enableNext: true,
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialValues(launchConfig, resource) {
|
||||
function getInitialValues(launchConfig, resourceDefaultCredentials) {
|
||||
if (!launchConfig.ask_credential_on_launch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
credentials: resource?.summary_fields?.credentials || [],
|
||||
credentials: resourceDefaultCredentials || [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -43,14 +43,21 @@ export default function useLaunchSteps(
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resource,
|
||||
i18n
|
||||
i18n,
|
||||
resourceDefaultCredentials
|
||||
) {
|
||||
const [visited, setVisited] = useState({});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const { touched, values: formikValues } = useFormikContext();
|
||||
const steps = [
|
||||
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||
useCredentialsStep(launchConfig, resource, i18n),
|
||||
useCredentialsStep(
|
||||
launchConfig,
|
||||
resource,
|
||||
resourceDefaultCredentials,
|
||||
i18n,
|
||||
true
|
||||
),
|
||||
useCredentialPasswordsStep(
|
||||
launchConfig,
|
||||
i18n,
|
||||
|
||||
@ -26,6 +26,7 @@ function Schedule({
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
hasDaysToKeepField,
|
||||
resourceDefaultCredentials,
|
||||
}) {
|
||||
const { scheduleId } = useParams();
|
||||
|
||||
@ -114,6 +115,7 @@ function Schedule({
|
||||
resource={resource}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
</Route>,
|
||||
<Route
|
||||
|
||||
@ -22,6 +22,7 @@ function ScheduleAdd({
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
hasDaysToKeepField,
|
||||
resourceDefaultCredentials,
|
||||
}) {
|
||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||
const history = useHistory();
|
||||
@ -117,6 +118,7 @@ function ScheduleAdd({
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resource={resource}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@ -22,6 +22,7 @@ function ScheduleEdit({
|
||||
resource,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resourceDefaultCredentials,
|
||||
}) {
|
||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||
const history = useHistory();
|
||||
@ -131,6 +132,7 @@ function ScheduleEdit({
|
||||
resource={resource}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@ -30,8 +30,20 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
SchedulesAPI.readCredentials.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ name: 'schedule credential 1', id: 1, kind: 'vault' },
|
||||
{ name: 'schedule credential 2', id: 2, kind: 'aws' },
|
||||
{
|
||||
name: 'schedule credential 1',
|
||||
id: 1,
|
||||
kind: 'vault',
|
||||
credential_type: 3,
|
||||
inputs: {},
|
||||
},
|
||||
{
|
||||
name: 'schedule credential 2',
|
||||
id: 2,
|
||||
kind: 'aws',
|
||||
credential_type: 4,
|
||||
inputs: {},
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
@ -45,9 +57,9 @@ CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 3,
|
||||
results: [
|
||||
{ id: 1, name: 'Credential 1', kind: 'ssh', url: '' },
|
||||
{ id: 2, name: 'Credential 2', kind: 'ssh', url: '' },
|
||||
{ id: 3, name: 'Credential 3', kind: 'ssh', url: '' },
|
||||
{ id: 1, name: 'Credential 1', kind: 'ssh', url: '', credential_type: 1 },
|
||||
{ id: 2, name: 'Credential 2', kind: 'ssh', url: '', credential_type: 1 },
|
||||
{ id: 3, name: 'Credential 3', kind: 'ssh', url: '', credential_type: 1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
@ -115,6 +127,7 @@ describe('<ScheduleEdit />', () => {
|
||||
],
|
||||
},
|
||||
}}
|
||||
resourceDefaultCredentials={[]}
|
||||
launchConfig={{
|
||||
can_start_without_user_input: false,
|
||||
passwords_needed_to_start: [],
|
||||
@ -150,6 +163,7 @@ describe('<ScheduleEdit />', () => {
|
||||
id: null,
|
||||
},
|
||||
scm_branch: '',
|
||||
credentials: [],
|
||||
},
|
||||
}}
|
||||
surveyConfig={{}}
|
||||
@ -466,7 +480,7 @@ describe('<ScheduleEdit />', () => {
|
||||
.prop('isCurrent')
|
||||
).toBe(true);
|
||||
|
||||
expect(wrapper.find('CredentialChip').length).toBe(3);
|
||||
expect(wrapper.find('CredentialChip').length).toBe(2);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ function Schedules({
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resource,
|
||||
resourceDefaultCredentials,
|
||||
}) {
|
||||
const match = useRouteMatch();
|
||||
|
||||
@ -32,6 +33,7 @@ function Schedules({
|
||||
resource={resource}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
</Route>
|
||||
<Route key="details" path={`${match.path}/:scheduleId`}>
|
||||
@ -41,6 +43,7 @@ function Schedules({
|
||||
resource={resource}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
</Route>
|
||||
<Route key="list" path={`${match.path}`}>
|
||||
|
||||
@ -204,6 +204,7 @@ function ScheduleForm({
|
||||
resource,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
resourceDefaultCredentials,
|
||||
...rest
|
||||
}) {
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
@ -297,11 +298,84 @@ function ScheduleForm({
|
||||
return missingValues;
|
||||
}, [launchConfig, schedule, surveyConfig]);
|
||||
|
||||
const hasCredentialsThatPrompt = useCallback(() => {
|
||||
if (launchConfig?.ask_credential_on_launch) {
|
||||
if (Object.keys(schedule).length > 0) {
|
||||
const defaultCredsWithoutOverrides = [];
|
||||
|
||||
const credentialHasOverride = templateDefaultCred => {
|
||||
let hasOverride = false;
|
||||
credentials.forEach(nodeCredential => {
|
||||
if (
|
||||
templateDefaultCred.credential_type ===
|
||||
nodeCredential.credential_type
|
||||
) {
|
||||
if (
|
||||
(!templateDefaultCred.vault_id &&
|
||||
!nodeCredential.inputs.vault_id) ||
|
||||
(templateDefaultCred.vault_id &&
|
||||
nodeCredential.inputs.vault_id &&
|
||||
templateDefaultCred.vault_id ===
|
||||
nodeCredential.inputs.vault_id)
|
||||
) {
|
||||
hasOverride = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hasOverride;
|
||||
};
|
||||
|
||||
if (resourceDefaultCredentials) {
|
||||
resourceDefaultCredentials.forEach(defaultCred => {
|
||||
if (!credentialHasOverride(defaultCred)) {
|
||||
defaultCredsWithoutOverrides.push(defaultCred);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
credentials
|
||||
.concat(defaultCredsWithoutOverrides)
|
||||
.filter(credential => {
|
||||
let credentialRequiresPass = false;
|
||||
|
||||
Object.entries(credential.inputs).forEach(([key, value]) => {
|
||||
if (key !== 'vault_id' && value === 'ASK') {
|
||||
credentialRequiresPass = true;
|
||||
}
|
||||
});
|
||||
|
||||
return credentialRequiresPass;
|
||||
}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
return launchConfig?.defaults?.credentials
|
||||
? launchConfig.defaults.credentials.filter(
|
||||
credential => credential?.passwords_needed.length > 0
|
||||
).length > 0
|
||||
: false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [launchConfig, schedule, credentials, resourceDefaultCredentials]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTemplate && (missingRequiredInventory() || hasMissingSurveyValue())) {
|
||||
if (
|
||||
isTemplate &&
|
||||
(missingRequiredInventory() ||
|
||||
hasMissingSurveyValue() ||
|
||||
hasCredentialsThatPrompt())
|
||||
) {
|
||||
setIsSaveDisabled(true);
|
||||
}
|
||||
}, [isTemplate, hasMissingSurveyValue, missingRequiredInventory]);
|
||||
}, [
|
||||
isTemplate,
|
||||
hasMissingSurveyValue,
|
||||
missingRequiredInventory,
|
||||
hasCredentialsThatPrompt,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
loadScheduleData();
|
||||
@ -527,14 +601,14 @@ function ScheduleForm({
|
||||
surveyConfig={surveyConfig}
|
||||
launchConfig={launchConfig}
|
||||
resource={resource}
|
||||
onCloseWizard={hasErrors => {
|
||||
onCloseWizard={() => {
|
||||
setIsWizardOpen(false);
|
||||
setIsSaveDisabled(hasErrors);
|
||||
}}
|
||||
onSave={() => {
|
||||
setIsWizardOpen(false);
|
||||
setIsSaveDisabled(false);
|
||||
}}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
)}
|
||||
<FormSubmitError error={submitError} />
|
||||
|
||||
@ -17,10 +17,10 @@ function SchedulePromptableFields({
|
||||
onSave,
|
||||
credentials,
|
||||
resource,
|
||||
resourceDefaultCredentials,
|
||||
i18n,
|
||||
}) {
|
||||
const {
|
||||
validateForm,
|
||||
setFieldTouched,
|
||||
values,
|
||||
initialValues,
|
||||
@ -39,12 +39,12 @@ function SchedulePromptableFields({
|
||||
schedule,
|
||||
resource,
|
||||
i18n,
|
||||
credentials
|
||||
credentials,
|
||||
resourceDefaultCredentials
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(contentError);
|
||||
const cancelPromptableValues = async () => {
|
||||
const hasErrors = await validateForm();
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
@ -66,7 +66,7 @@ function SchedulePromptableFields({
|
||||
timezone: values.timezone,
|
||||
},
|
||||
});
|
||||
onCloseWizard(Object.keys(hasErrors).length > 0);
|
||||
onCloseWizard();
|
||||
};
|
||||
|
||||
if (error) {
|
||||
@ -89,13 +89,16 @@ function SchedulePromptableFields({
|
||||
isOpen
|
||||
onClose={cancelPromptableValues}
|
||||
onSave={onSave}
|
||||
onBack={async nextStep => {
|
||||
validateStep(nextStep.id);
|
||||
}}
|
||||
onNext={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
onGoToStep={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
@ -104,7 +107,6 @@ function SchedulePromptableFields({
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
title={i18n._(t`Prompts`)}
|
||||
steps={
|
||||
|
||||
@ -13,29 +13,28 @@ export default function useSchedulePromptSteps(
|
||||
schedule,
|
||||
resource,
|
||||
i18n,
|
||||
scheduleCredentials
|
||||
scheduleCredentials,
|
||||
resourceDefaultCredentials
|
||||
) {
|
||||
const {
|
||||
summary_fields: { credentials: resourceCredentials },
|
||||
} = resource;
|
||||
const sourceOfValues =
|
||||
(Object.keys(schedule).length > 0 && schedule) || resource;
|
||||
|
||||
sourceOfValues.summary_fields = {
|
||||
credentials: [...(resourceCredentials || []), ...scheduleCredentials],
|
||||
...sourceOfValues.summary_fields,
|
||||
};
|
||||
const { resetForm, values } = useFormikContext();
|
||||
const [visited, setVisited] = useState({});
|
||||
|
||||
const steps = [
|
||||
useInventoryStep(launchConfig, sourceOfValues, i18n, visited),
|
||||
useCredentialsStep(launchConfig, sourceOfValues, i18n),
|
||||
useCredentialsStep(
|
||||
launchConfig,
|
||||
sourceOfValues,
|
||||
resourceDefaultCredentials,
|
||||
i18n
|
||||
),
|
||||
useOtherPromptsStep(launchConfig, sourceOfValues, i18n),
|
||||
useSurveyStep(launchConfig, surveyConfig, sourceOfValues, i18n, visited),
|
||||
];
|
||||
|
||||
const hasErrors = steps.some(step => step.hasError);
|
||||
|
||||
steps.push(
|
||||
usePreviewStep(
|
||||
launchConfig,
|
||||
@ -52,21 +51,61 @@ export default function useSchedulePromptSteps(
|
||||
const isReady = !steps.some(s => !s.isReady);
|
||||
|
||||
useEffect(() => {
|
||||
let initialValues = {};
|
||||
if (launchConfig && surveyConfig && isReady) {
|
||||
let initialValues = {};
|
||||
initialValues = steps.reduce((acc, cur) => {
|
||||
return {
|
||||
...acc,
|
||||
...cur.initialValues,
|
||||
};
|
||||
}, {});
|
||||
|
||||
if (launchConfig.ask_credential_on_launch) {
|
||||
const defaultCredsWithoutOverrides = [];
|
||||
|
||||
const credentialHasOverride = templateDefaultCred => {
|
||||
let hasOverride = false;
|
||||
scheduleCredentials.forEach(scheduleCredential => {
|
||||
if (
|
||||
templateDefaultCred.credential_type ===
|
||||
scheduleCredential.credential_type
|
||||
) {
|
||||
if (
|
||||
(!templateDefaultCred.inputs.vault_id &&
|
||||
!scheduleCredential.inputs.vault_id) ||
|
||||
(templateDefaultCred.inputs.vault_id &&
|
||||
scheduleCredential.inputs.vault_id &&
|
||||
templateDefaultCred.inputs.vault_id ===
|
||||
scheduleCredential.inputs.vault_id)
|
||||
) {
|
||||
hasOverride = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hasOverride;
|
||||
};
|
||||
|
||||
if (resourceDefaultCredentials) {
|
||||
resourceDefaultCredentials.forEach(defaultCred => {
|
||||
if (!credentialHasOverride(defaultCred)) {
|
||||
defaultCredsWithoutOverrides.push(defaultCred);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initialValues.credentials = scheduleCredentials.concat(
|
||||
defaultCredsWithoutOverrides
|
||||
);
|
||||
}
|
||||
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
}
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [launchConfig, surveyConfig, isReady]);
|
||||
|
||||
@ -6,7 +6,7 @@ import styled from 'styled-components';
|
||||
import ChipGroup from '../ChipGroup';
|
||||
|
||||
const Split = styled(PFSplit)`
|
||||
margin: 20px 0 5px 0;
|
||||
margin: 20px 0 5px 0 !important;
|
||||
align-items: baseline;
|
||||
`;
|
||||
|
||||
|
||||
@ -32,7 +32,13 @@ function Template({ i18n, setBreadcrumb }) {
|
||||
const { me = {} } = useConfig();
|
||||
|
||||
const {
|
||||
result: { isNotifAdmin, template, surveyConfig, launchConfig },
|
||||
result: {
|
||||
isNotifAdmin,
|
||||
template,
|
||||
surveyConfig,
|
||||
launchConfig,
|
||||
resourceDefaultCredentials,
|
||||
},
|
||||
isLoading,
|
||||
error: contentError,
|
||||
request: loadTemplateAndRoles,
|
||||
@ -40,11 +46,17 @@ function Template({ i18n, setBreadcrumb }) {
|
||||
useCallback(async () => {
|
||||
const [
|
||||
{ data },
|
||||
{
|
||||
data: { results: defaultCredentials },
|
||||
},
|
||||
actions,
|
||||
notifAdminRes,
|
||||
{ data: launchConfiguration },
|
||||
] = await Promise.all([
|
||||
JobTemplatesAPI.readDetail(templateId),
|
||||
JobTemplatesAPI.readCredentials(templateId, {
|
||||
page_size: 200,
|
||||
}),
|
||||
JobTemplatesAPI.readTemplateOptions(templateId),
|
||||
OrganizationsAPI.read({
|
||||
page_size: 1,
|
||||
@ -52,7 +64,7 @@ function Template({ i18n, setBreadcrumb }) {
|
||||
}),
|
||||
JobTemplatesAPI.readLaunch(templateId),
|
||||
]);
|
||||
let surveyConfiguration = null;
|
||||
let surveyConfiguration = {};
|
||||
|
||||
if (data.survey_enabled) {
|
||||
const { data: survey } = await JobTemplatesAPI.readSurvey(templateId);
|
||||
@ -86,9 +98,10 @@ function Template({ i18n, setBreadcrumb }) {
|
||||
isNotifAdmin: notifAdminRes.data.results.length > 0,
|
||||
surveyConfig: surveyConfiguration,
|
||||
launchConfig: launchConfiguration,
|
||||
resourceDefaultCredentials: defaultCredentials,
|
||||
};
|
||||
}, [templateId]),
|
||||
{ isNotifAdmin: false, template: null }
|
||||
{ isNotifAdmin: false, template: null, resourceDefaultCredentials: [] }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -221,6 +234,7 @@ function Template({ i18n, setBreadcrumb }) {
|
||||
loadScheduleOptions={loadScheduleOptions}
|
||||
surveyConfig={surveyConfig}
|
||||
launchConfig={launchConfig}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
/>
|
||||
</Route>
|
||||
{canSeeNotificationsTab && (
|
||||
|
||||
@ -41,6 +41,7 @@ function NodeModalForm({
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
isLaunchLoading,
|
||||
resourceDefaultCredentials,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
@ -69,7 +70,8 @@ function NodeModalForm({
|
||||
surveyConfig,
|
||||
i18n,
|
||||
values.nodeResource,
|
||||
askLinkType
|
||||
askLinkType,
|
||||
resourceDefaultCredentials
|
||||
);
|
||||
|
||||
const handleSaveNode = () => {
|
||||
@ -229,7 +231,7 @@ const NodeModalInner = ({ i18n, title, ...rest }) => {
|
||||
const {
|
||||
request: readLaunchConfigs,
|
||||
error: launchConfigError,
|
||||
result: { launchConfig, surveyConfig },
|
||||
result: { launchConfig, surveyConfig, resourceDefaultCredentials },
|
||||
isLoading,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
@ -247,6 +249,7 @@ const NodeModalInner = ({ i18n, title, ...rest }) => {
|
||||
return {
|
||||
launchConfig: {},
|
||||
surveyConfig: {},
|
||||
resourceDefaultCredentials: [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -267,9 +270,21 @@ const NodeModalInner = ({ i18n, title, ...rest }) => {
|
||||
survey = data;
|
||||
}
|
||||
|
||||
let defaultCredentials = [];
|
||||
|
||||
if (launch.ask_credential_on_launch) {
|
||||
const {
|
||||
data: { results },
|
||||
} = await JobTemplatesAPI.readCredentials(values?.nodeResource?.id, {
|
||||
page_size: 200,
|
||||
});
|
||||
defaultCredentials = results;
|
||||
}
|
||||
|
||||
return {
|
||||
launchConfig: launch,
|
||||
surveyConfig: survey,
|
||||
resourceDefaultCredentials: defaultCredentials,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -319,6 +334,7 @@ const NodeModalInner = ({ i18n, title, ...rest }) => {
|
||||
{...rest}
|
||||
launchConfig={launchConfig}
|
||||
surveyConfig={surveyConfig}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
isLaunchLoading={isLoading}
|
||||
title={wizardTitle}
|
||||
i18n={i18n}
|
||||
|
||||
@ -115,6 +115,11 @@ describe('NodeModal', () => {
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({ data: jtLaunchConfig });
|
||||
JobTemplatesAPI.readCredentials.mockResolvedValue({
|
||||
data: {
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readSurvey.mockResolvedValue({
|
||||
data: {
|
||||
name: '',
|
||||
@ -239,7 +244,12 @@ describe('NodeModal', () => {
|
||||
nodeToEdit: null,
|
||||
}}
|
||||
>
|
||||
<NodeModal askLinkType onSave={onSave} title="Add Node" />
|
||||
<NodeModal
|
||||
askLinkType
|
||||
onSave={onSave}
|
||||
title="Add Node"
|
||||
resourceDefaultCredentials={[]}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
);
|
||||
@ -254,8 +264,9 @@ describe('NodeModal', () => {
|
||||
|
||||
test('Can successfully create a new job template node', async () => {
|
||||
act(() => {
|
||||
wrapper.find('#link-type-always').simulate('click');
|
||||
wrapper.find('SelectableCard#link-type-always').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
});
|
||||
@ -271,6 +282,9 @@ describe('NodeModal', () => {
|
||||
wrapper.update();
|
||||
|
||||
expect(JobTemplatesAPI.readLaunch).toBeCalledWith(1);
|
||||
expect(JobTemplatesAPI.readCredentials).toBeCalledWith(1, {
|
||||
page_size: 200,
|
||||
});
|
||||
expect(JobTemplatesAPI.readSurvey).toBeCalledWith(25);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('NodeNextButton').prop('buttonText')).toBe('Next');
|
||||
@ -281,11 +295,6 @@ describe('NodeModal', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(JobTemplatesAPI.readLaunch).toBeCalledWith(1);
|
||||
expect(JobTemplatesAPI.readSurvey).toBeCalledWith(25);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('NodeNextButton').prop('buttonText')).toBe('Save');
|
||||
act(() => {
|
||||
@ -317,7 +326,7 @@ describe('NodeModal', () => {
|
||||
|
||||
test('Can successfully create a new project sync node', async () => {
|
||||
act(() => {
|
||||
wrapper.find('#link-type-failure').simulate('click');
|
||||
wrapper.find('SelectableCard#link-type-failure').simulate('click');
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
@ -352,7 +361,7 @@ describe('NodeModal', () => {
|
||||
|
||||
test('Can successfully create a new inventory source sync node', async () => {
|
||||
act(() => {
|
||||
wrapper.find('#link-type-failure').simulate('click');
|
||||
wrapper.find('SelectableCard#link-type-failure').simulate('click');
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
@ -446,7 +455,7 @@ describe('NodeModal', () => {
|
||||
|
||||
test('Can successfully create a new approval template node', async () => {
|
||||
act(() => {
|
||||
wrapper.find('#link-type-always').simulate('click');
|
||||
wrapper.find('SelectableCard#link-type-always').simulate('click');
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('button#next-node-modal').simulate('click');
|
||||
@ -543,6 +552,7 @@ describe('NodeModal', () => {
|
||||
askLinkType={false}
|
||||
onSave={onSave}
|
||||
title="Edit Node"
|
||||
resourceDefaultCredentials={[]}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
@ -629,6 +639,7 @@ describe('NodeModal', () => {
|
||||
askLinkType={false}
|
||||
onSave={onSave}
|
||||
title="Edit Node"
|
||||
resourceDefaultCredentials={[]}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
|
||||
@ -3,7 +3,6 @@ import { useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { Tooltip } from '@patternfly/react-core';
|
||||
import { JobTemplatesAPI } from '../../../../../../api';
|
||||
import { getQSConfig, parseQueryString } from '../../../../../../util/qs';
|
||||
import useRequest from '../../../../../../util/useRequest';
|
||||
@ -57,56 +56,26 @@ function JobTemplatesList({ i18n, nodeResource, onUpdateNodeResource }) {
|
||||
fetchJobTemplates();
|
||||
}, [fetchJobTemplates]);
|
||||
|
||||
const onSelectRow = row => {
|
||||
if (
|
||||
row.project &&
|
||||
row.project !== null &&
|
||||
((row.inventory && row.inventory !== null) || row.ask_inventory_on_launch)
|
||||
) {
|
||||
onUpdateNodeResource(row);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PaginatedDataList
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
itemCount={count}
|
||||
items={jobTemplates}
|
||||
onRowClick={row => onSelectRow(row)}
|
||||
onRowClick={row => onUpdateNodeResource(row)}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItem={item => {
|
||||
const isDisabled =
|
||||
!item.project ||
|
||||
item.project === null ||
|
||||
((!item.inventory || item.inventory === null) &&
|
||||
!item.ask_inventory_on_launch);
|
||||
const listItem = (
|
||||
<CheckboxListItem
|
||||
isDisabled={isDisabled}
|
||||
isSelected={!!(nodeResource && nodeResource.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={`${item.id}-listItem`}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => onSelectRow(item)}
|
||||
onDeselect={() => onUpdateNodeResource(null)}
|
||||
isRadio
|
||||
/>
|
||||
);
|
||||
return isDisabled ? (
|
||||
<Tooltip
|
||||
key={`${item.id}-tooltip`}
|
||||
content={i18n._(
|
||||
t`Job Templates with a missing inventory or project cannot be selected when creating or editing nodes`
|
||||
)}
|
||||
>
|
||||
{listItem}
|
||||
</Tooltip>
|
||||
) : (
|
||||
listItem
|
||||
);
|
||||
}}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={!!(nodeResource && nodeResource.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={`${item.id}-listItem`}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => onUpdateNodeResource(item)}
|
||||
onDeselect={() => onUpdateNodeResource(null)}
|
||||
isRadio
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
toolbarSearchColumns={[
|
||||
|
||||
@ -61,22 +61,15 @@ describe('JobTemplatesList', () => {
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
// expect(wrapper.debug()).toBe(false);
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template"]').props()
|
||||
.isSelected
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template"]').props()
|
||||
.isDisabled
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props()
|
||||
.isSelected
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props()
|
||||
.isDisabled
|
||||
).toBe(false);
|
||||
wrapper
|
||||
.find('CheckboxListItem[name="Test Job Template 2"]')
|
||||
.simulate('click');
|
||||
@ -89,71 +82,6 @@ describe('JobTemplatesList', () => {
|
||||
project: 2,
|
||||
});
|
||||
});
|
||||
test('Row disabled when job template missing inventory or project', async () => {
|
||||
JobTemplatesAPI.read.mockResolvedValueOnce({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test Job Template',
|
||||
type: 'job_template',
|
||||
url: '/api/v2/job_templates/1',
|
||||
inventory: 1,
|
||||
project: null,
|
||||
ask_inventory_on_launch: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Test Job Template 2',
|
||||
type: 'job_template',
|
||||
url: '/api/v2/job_templates/2',
|
||||
inventory: null,
|
||||
project: 2,
|
||||
ask_inventory_on_launch: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
related_search_fields: [],
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<JobTemplatesList
|
||||
nodeResource={nodeResource}
|
||||
onUpdateNodeResource={onUpdateNodeResource}
|
||||
/>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template"]').props()
|
||||
.isSelected
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template"]').props()
|
||||
.isDisabled
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props()
|
||||
.isSelected
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props()
|
||||
.isDisabled
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('CheckboxListItem[name="Test Job Template 2"]')
|
||||
.simulate('click');
|
||||
expect(onUpdateNodeResource).not.toHaveBeenCalled();
|
||||
});
|
||||
test('Error shown when read() request errors', async () => {
|
||||
JobTemplatesAPI.read.mockRejectedValue(new Error());
|
||||
JobTemplatesAPI.readOptions.mockResolvedValue({
|
||||
|
||||
@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { useField } from 'formik';
|
||||
import { Form, FormGroup, TextInput } from '@patternfly/react-core';
|
||||
import { Alert, Form, FormGroup, TextInput } from '@patternfly/react-core';
|
||||
import { required } from '../../../../../../util/validators';
|
||||
|
||||
import { FormFullWidthLayout } from '../../../../../../components/FormLayout';
|
||||
@ -15,6 +15,10 @@ import ProjectsList from './ProjectsList';
|
||||
import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
|
||||
import FormField from '../../../../../../components/FormField';
|
||||
|
||||
const NodeTypeErrorAlert = styled(Alert)`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const TimeoutInput = styled(TextInput)`
|
||||
width: 200px;
|
||||
:not(:first-of-type) {
|
||||
@ -29,7 +33,9 @@ const TimeoutLabel = styled.p`
|
||||
|
||||
function NodeTypeStep({ i18n }) {
|
||||
const [nodeTypeField, , nodeTypeHelpers] = useField('nodeType');
|
||||
const [nodeResourceField, , nodeResourceHelpers] = useField('nodeResource');
|
||||
const [nodeResourceField, nodeResourceMeta, nodeResourceHelpers] = useField(
|
||||
'nodeResource'
|
||||
);
|
||||
const [, approvalNameMeta, approvalNameHelpers] = useField('approvalName');
|
||||
const [, , approvalDescriptionHelpers] = useField('approvalDescription');
|
||||
const [timeoutMinutesField, , timeoutMinutesHelpers] = useField(
|
||||
@ -42,6 +48,13 @@ function NodeTypeStep({ i18n }) {
|
||||
const isValid = !approvalNameMeta.touched || !approvalNameMeta.error;
|
||||
return (
|
||||
<>
|
||||
{nodeResourceMeta.error && (
|
||||
<NodeTypeErrorAlert
|
||||
variant="danger"
|
||||
isInline
|
||||
title={nodeResourceMeta.error}
|
||||
/>
|
||||
)}
|
||||
<div css="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<b css="margin-right: 24px">{i18n._(t`Node Type`)}</b>
|
||||
<div>
|
||||
|
||||
@ -6,31 +6,62 @@ import StepName from '../../../../../../components/LaunchPrompt/steps/StepName';
|
||||
|
||||
const STEP_ID = 'nodeType';
|
||||
|
||||
export default function useNodeTypeStep(i18n) {
|
||||
export default function useNodeTypeStep(launchConfig, i18n) {
|
||||
const [, meta] = useField('nodeType');
|
||||
const [approvalNameField] = useField('approvalName');
|
||||
const [nodeTypeField, ,] = useField('nodeType');
|
||||
const [nodeResourceField] = useField('nodeResource');
|
||||
const [nodeResourceField, nodeResourceMeta] = useField({
|
||||
name: 'nodeResource',
|
||||
validate: value => {
|
||||
if (
|
||||
value?.type === 'job_template' &&
|
||||
(!value?.project ||
|
||||
value?.project === null ||
|
||||
((!value?.inventory || value?.inventory === null) &&
|
||||
!value?.ask_inventory_on_launch))
|
||||
) {
|
||||
return i18n._(
|
||||
t`Job Templates with a missing inventory or project cannot be selected when creating or editing nodes. Select another template or fix the missing fields to proceed.`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const formError = !!meta.error || !!nodeResourceMeta.error;
|
||||
|
||||
return {
|
||||
step: getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField),
|
||||
step: getStep(
|
||||
i18n,
|
||||
nodeTypeField,
|
||||
approvalNameField,
|
||||
nodeResourceField,
|
||||
formError
|
||||
),
|
||||
initialValues: getInitialValues(),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
hasError: !!meta.error,
|
||||
hasError: formError,
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('nodeType', true, false);
|
||||
},
|
||||
validate: () => {},
|
||||
};
|
||||
}
|
||||
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||
function getStep(
|
||||
i18n,
|
||||
nodeTypeField,
|
||||
approvalNameField,
|
||||
nodeResourceField,
|
||||
formError
|
||||
) {
|
||||
const isEnabled = () => {
|
||||
if (
|
||||
(nodeTypeField.value !== 'workflow_approval_template' &&
|
||||
nodeResourceField.value === null) ||
|
||||
(nodeTypeField.value === 'workflow_approval_template' &&
|
||||
approvalNameField.value === undefined)
|
||||
approvalNameField.value === undefined) ||
|
||||
formError
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@ -39,7 +70,7 @@ function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||
return {
|
||||
id: STEP_ID,
|
||||
name: (
|
||||
<StepName hasErrors={false} id="node-type-step">
|
||||
<StepName hasErrors={formError} id="node-type-step">
|
||||
{i18n._(t`Node type`)}
|
||||
</StepName>
|
||||
),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useContext, useState, useEffect } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { t } from '@lingui/macro';
|
||||
import useInventoryStep from '../../../../../components/LaunchPrompt/steps/useInventoryStep';
|
||||
import useCredentialsStep from '../../../../../components/LaunchPrompt/steps/useCredentialsStep';
|
||||
import useOtherPromptsStep from '../../../../../components/LaunchPrompt/steps/useOtherPromptsStep';
|
||||
@ -29,7 +30,12 @@ function showPreviewStep(nodeType, launchConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
const getNodeToEditDefaultValues = (launchConfig, surveyConfig, nodeToEdit) => {
|
||||
const getNodeToEditDefaultValues = (
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
nodeToEdit,
|
||||
resourceDefaultCredentials
|
||||
) => {
|
||||
const initialValues = {
|
||||
nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null,
|
||||
nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template',
|
||||
@ -70,35 +76,34 @@ const getNodeToEditDefaultValues = (launchConfig, surveyConfig, nodeToEdit) => {
|
||||
} else if (nodeToEdit?.originalNodeCredentials) {
|
||||
const defaultCredsWithoutOverrides = [];
|
||||
|
||||
const credentialHasScheduleOverride = templateDefaultCred => {
|
||||
let credentialHasOverride = false;
|
||||
nodeToEdit.originalNodeCredentials.forEach(scheduleCred => {
|
||||
const credentialHasOverride = templateDefaultCred => {
|
||||
let hasOverride = false;
|
||||
nodeToEdit.originalNodeCredentials.forEach(nodeCredential => {
|
||||
if (
|
||||
templateDefaultCred.credential_type === scheduleCred.credential_type
|
||||
templateDefaultCred.credential_type ===
|
||||
nodeCredential.credential_type
|
||||
) {
|
||||
if (
|
||||
(!templateDefaultCred.vault_id &&
|
||||
!scheduleCred.inputs.vault_id) ||
|
||||
!nodeCredential.inputs.vault_id) ||
|
||||
(templateDefaultCred.vault_id &&
|
||||
scheduleCred.inputs.vault_id &&
|
||||
templateDefaultCred.vault_id === scheduleCred.inputs.vault_id)
|
||||
nodeCredential.inputs.vault_id &&
|
||||
templateDefaultCred.vault_id === nodeCredential.inputs.vault_id)
|
||||
) {
|
||||
credentialHasOverride = true;
|
||||
hasOverride = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return credentialHasOverride;
|
||||
return hasOverride;
|
||||
};
|
||||
|
||||
if (nodeToEdit?.fullUnifiedJobTemplate?.summary_fields?.credentials) {
|
||||
nodeToEdit.fullUnifiedJobTemplate.summary_fields.credentials.forEach(
|
||||
defaultCred => {
|
||||
if (!credentialHasScheduleOverride(defaultCred)) {
|
||||
defaultCredsWithoutOverrides.push(defaultCred);
|
||||
}
|
||||
if (resourceDefaultCredentials) {
|
||||
resourceDefaultCredentials.forEach(defaultCred => {
|
||||
if (!credentialHasOverride(defaultCred)) {
|
||||
defaultCredsWithoutOverrides.push(defaultCred);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
initialValues.credentials = nodeToEdit.originalNodeCredentials.concat(
|
||||
@ -179,17 +184,27 @@ export default function useWorkflowNodeSteps(
|
||||
surveyConfig,
|
||||
i18n,
|
||||
resource,
|
||||
askLinkType
|
||||
askLinkType,
|
||||
resourceDefaultCredentials
|
||||
) {
|
||||
const { nodeToEdit } = useContext(WorkflowStateContext);
|
||||
const { resetForm, values: formikValues } = useFormikContext();
|
||||
const {
|
||||
resetForm,
|
||||
values: formikValues,
|
||||
errors: formikErrors,
|
||||
} = useFormikContext();
|
||||
const [visited, setVisited] = useState({});
|
||||
|
||||
const steps = [
|
||||
useRunTypeStep(i18n, askLinkType),
|
||||
useNodeTypeStep(i18n),
|
||||
useNodeTypeStep(launchConfig, i18n),
|
||||
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||
useCredentialsStep(launchConfig, resource, i18n),
|
||||
useCredentialsStep(
|
||||
launchConfig,
|
||||
resource,
|
||||
resourceDefaultCredentials,
|
||||
i18n
|
||||
),
|
||||
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||
];
|
||||
@ -222,7 +237,8 @@ export default function useWorkflowNodeSteps(
|
||||
initialValues = getNodeToEditDefaultValues(
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
nodeToEdit
|
||||
nodeToEdit,
|
||||
resourceDefaultCredentials
|
||||
);
|
||||
} else {
|
||||
initialValues = steps.reduce((acc, cur) => {
|
||||
@ -233,7 +249,23 @@ export default function useWorkflowNodeSteps(
|
||||
}, {});
|
||||
}
|
||||
|
||||
const errors = formikErrors.nodeResource
|
||||
? {
|
||||
nodeResource: formikErrors.nodeResource,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (
|
||||
!launchConfig?.ask_credential_on_launch &&
|
||||
launchConfig?.passwords_needed_to_start?.length > 0
|
||||
) {
|
||||
errors.nodeResource = i18n._(
|
||||
t`Job Templates with credentials that prompt for passwords cannot be selected when creating or editing nodes`
|
||||
);
|
||||
}
|
||||
|
||||
resetForm({
|
||||
errors,
|
||||
values: {
|
||||
...initialValues,
|
||||
nodeResource: formikValues.nodeResource,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user