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