mirror of
https://github.com/ansible/awx.git
synced 2026-05-15 05:17:36 -02:30
Merge pull request #9028 from mabashian/7015-prompt-cred-passwords-v2
Add support for credential password prompting on job launch Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -12,7 +12,15 @@ import {
|
|||||||
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
|
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
function PasswordInput(props) {
|
function PasswordInput(props) {
|
||||||
const { id, name, validate, isRequired, isDisabled, i18n } = props;
|
const {
|
||||||
|
autocomplete,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
validate,
|
||||||
|
isRequired,
|
||||||
|
isDisabled,
|
||||||
|
i18n,
|
||||||
|
} = props;
|
||||||
const [inputType, setInputType] = useState('password');
|
const [inputType, setInputType] = useState('password');
|
||||||
const [field, meta] = useField({ name, validate });
|
const [field, meta] = useField({ name, validate });
|
||||||
|
|
||||||
@@ -38,6 +46,7 @@ function PasswordInput(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
autoComplete={autocomplete}
|
||||||
id={id}
|
id={id}
|
||||||
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
|
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
|
||||||
{...field}
|
{...field}
|
||||||
@@ -55,6 +64,7 @@ function PasswordInput(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
|
autocomplete: PropTypes.string,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
validate: PropTypes.func,
|
validate: PropTypes.func,
|
||||||
@@ -63,6 +73,7 @@ PasswordInput.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
PasswordInput.defaultProps = {
|
||||||
|
autocomplete: 'new-password',
|
||||||
validate: () => {},
|
validate: () => {},
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ function canLaunchWithoutPrompt(launchData) {
|
|||||||
!launchData.ask_limit_on_launch &&
|
!launchData.ask_limit_on_launch &&
|
||||||
!launchData.ask_scm_branch_on_launch &&
|
!launchData.ask_scm_branch_on_launch &&
|
||||||
!launchData.survey_enabled &&
|
!launchData.survey_enabled &&
|
||||||
|
(!launchData.passwords_needed_to_start ||
|
||||||
|
launchData.passwords_needed_to_start.length === 0) &&
|
||||||
(!launchData.variables_needed_to_start ||
|
(!launchData.variables_needed_to_start ||
|
||||||
launchData.variables_needed_to_start.length === 0)
|
launchData.variables_needed_to_start.length === 0)
|
||||||
);
|
);
|
||||||
@@ -100,17 +102,20 @@ class LaunchButton extends React.Component {
|
|||||||
async launchWithParams(params) {
|
async launchWithParams(params) {
|
||||||
try {
|
try {
|
||||||
const { history, resource } = this.props;
|
const { history, resource } = this.props;
|
||||||
const jobPromise =
|
let jobPromise;
|
||||||
resource.type === 'workflow_job_template'
|
|
||||||
? WorkflowJobTemplatesAPI.launch(resource.id, params || {})
|
if (resource.type === 'job_template') {
|
||||||
: JobTemplatesAPI.launch(resource.id, params || {});
|
jobPromise = JobTemplatesAPI.launch(resource.id, params || {});
|
||||||
|
} else if (resource.type === 'workflow_job_template') {
|
||||||
|
jobPromise = WorkflowJobTemplatesAPI.launch(resource.id, params || {});
|
||||||
|
} else if (resource.type === 'job') {
|
||||||
|
jobPromise = JobsAPI.relaunch(resource.id, params || {});
|
||||||
|
} else if (resource.type === 'workflow_job') {
|
||||||
|
jobPromise = WorkflowJobsAPI.relaunch(resource.id, params || {});
|
||||||
|
}
|
||||||
|
|
||||||
const { data: job } = await jobPromise;
|
const { data: job } = await jobPromise;
|
||||||
history.push(
|
history.push(`/jobs/${job.id}/output`);
|
||||||
`/${
|
|
||||||
resource.type === 'workflow_job_template' ? 'jobs/workflow' : 'jobs'
|
|
||||||
}/${job.id}/output`
|
|
||||||
);
|
|
||||||
} catch (launchError) {
|
} catch (launchError) {
|
||||||
this.setState({ launchError });
|
this.setState({ launchError });
|
||||||
}
|
}
|
||||||
@@ -127,20 +132,15 @@ class LaunchButton extends React.Component {
|
|||||||
readRelaunch = InventorySourcesAPI.readLaunchUpdate(
|
readRelaunch = InventorySourcesAPI.readLaunchUpdate(
|
||||||
resource.inventory_source
|
resource.inventory_source
|
||||||
);
|
);
|
||||||
relaunch = InventorySourcesAPI.launchUpdate(resource.inventory_source);
|
|
||||||
} else if (resource.type === 'project_update') {
|
} else if (resource.type === 'project_update') {
|
||||||
// We'll need to handle the scenario where the project no longer exists
|
// We'll need to handle the scenario where the project no longer exists
|
||||||
readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project);
|
readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project);
|
||||||
relaunch = ProjectsAPI.launchUpdate(resource.project);
|
|
||||||
} else if (resource.type === 'workflow_job') {
|
} else if (resource.type === 'workflow_job') {
|
||||||
readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id);
|
readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id);
|
||||||
relaunch = WorkflowJobsAPI.relaunch(resource.id);
|
|
||||||
} else if (resource.type === 'ad_hoc_command') {
|
} else if (resource.type === 'ad_hoc_command') {
|
||||||
readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id);
|
readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id);
|
||||||
relaunch = AdHocCommandsAPI.relaunch(resource.id);
|
|
||||||
} else if (resource.type === 'job') {
|
} else if (resource.type === 'job') {
|
||||||
readRelaunch = JobsAPI.readRelaunch(resource.id);
|
readRelaunch = JobsAPI.readRelaunch(resource.id);
|
||||||
relaunch = JobsAPI.relaunch(resource.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -149,11 +149,22 @@ class LaunchButton extends React.Component {
|
|||||||
!relaunchConfig.passwords_needed_to_start ||
|
!relaunchConfig.passwords_needed_to_start ||
|
||||||
relaunchConfig.passwords_needed_to_start.length === 0
|
relaunchConfig.passwords_needed_to_start.length === 0
|
||||||
) {
|
) {
|
||||||
|
if (resource.type === 'inventory_update') {
|
||||||
|
relaunch = InventorySourcesAPI.launchUpdate(
|
||||||
|
resource.inventory_source
|
||||||
|
);
|
||||||
|
} else if (resource.type === 'project_update') {
|
||||||
|
relaunch = ProjectsAPI.launchUpdate(resource.project);
|
||||||
|
} else if (resource.type === 'workflow_job') {
|
||||||
|
relaunch = WorkflowJobsAPI.relaunch(resource.id);
|
||||||
|
} else if (resource.type === 'ad_hoc_command') {
|
||||||
|
relaunch = AdHocCommandsAPI.relaunch(resource.id);
|
||||||
|
} else if (resource.type === 'job') {
|
||||||
|
relaunch = JobsAPI.relaunch(resource.id);
|
||||||
|
}
|
||||||
const { data: job } = await relaunch;
|
const { data: job } = await relaunch;
|
||||||
history.push(`/jobs/${job.id}/output`);
|
history.push(`/jobs/${job.id}/output`);
|
||||||
} else {
|
} else {
|
||||||
// TODO: restructure (async?) to send launch command after prompts
|
|
||||||
// TODO: does relaunch need different prompt treatment than launch?
|
|
||||||
this.setState({
|
this.setState({
|
||||||
showLaunchPrompt: true,
|
showLaunchPrompt: true,
|
||||||
launchConfig: relaunchConfig,
|
launchConfig: relaunchConfig,
|
||||||
|
|||||||
@@ -4,10 +4,16 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
import { sleep } from '../../../testUtils/testUtils';
|
import { sleep } from '../../../testUtils/testUtils';
|
||||||
|
|
||||||
import LaunchButton from './LaunchButton';
|
import LaunchButton from './LaunchButton';
|
||||||
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api';
|
import {
|
||||||
|
InventorySourcesAPI,
|
||||||
|
JobsAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
WorkflowJobsAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
|
} from '../../api';
|
||||||
|
|
||||||
jest.mock('../../api/models/WorkflowJobTemplates');
|
jest.mock('../../api');
|
||||||
jest.mock('../../api/models/JobTemplates');
|
|
||||||
|
|
||||||
describe('LaunchButton', () => {
|
describe('LaunchButton', () => {
|
||||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||||
@@ -22,10 +28,14 @@ describe('LaunchButton', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const children = ({ handleLaunch }) => (
|
const launchButton = ({ handleLaunch }) => (
|
||||||
<button type="submit" onClick={() => handleLaunch()} />
|
<button type="submit" onClick={() => handleLaunch()} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const relaunchButton = ({ handleRelaunch }) => (
|
||||||
|
<button type="submit" onClick={() => handleRelaunch()} />
|
||||||
|
);
|
||||||
|
|
||||||
const resource = {
|
const resource = {
|
||||||
id: 1,
|
id: 1,
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
@@ -35,7 +45,7 @@ describe('LaunchButton', () => {
|
|||||||
|
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
<LaunchButton resource={resource}>{launchButton}</LaunchButton>
|
||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
@@ -51,7 +61,7 @@ describe('LaunchButton', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<LaunchButton resource={resource}>{children}</LaunchButton>,
|
<LaunchButton resource={resource}>{launchButton}</LaunchButton>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
router: { history },
|
router: { history },
|
||||||
@@ -87,7 +97,7 @@ describe('LaunchButton', () => {
|
|||||||
type: 'workflow_job_template',
|
type: 'workflow_job_template',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{launchButton}
|
||||||
</LaunchButton>,
|
</LaunchButton>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
@@ -100,12 +110,162 @@ describe('LaunchButton', () => {
|
|||||||
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
expect(WorkflowJobTemplatesAPI.launch).toHaveBeenCalledWith(1, {});
|
expect(WorkflowJobTemplatesAPI.launch).toHaveBeenCalledWith(1, {});
|
||||||
expect(history.location.pathname).toEqual('/jobs/workflow/9000/output');
|
expect(history.location.pathname).toEqual('/jobs/9000/output');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should relaunch job correctly', async () => {
|
||||||
|
JobsAPI.readRelaunch.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
can_start_without_user_input: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/jobs/9000'],
|
||||||
|
});
|
||||||
|
JobsAPI.relaunch.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 9000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<LaunchButton
|
||||||
|
resource={{
|
||||||
|
id: 1,
|
||||||
|
type: 'job',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{relaunchButton}
|
||||||
|
</LaunchButton>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: { history },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const button = wrapper.find('button');
|
||||||
|
button.prop('onClick')();
|
||||||
|
expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1);
|
||||||
|
await sleep(0);
|
||||||
|
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1);
|
||||||
|
expect(history.location.pathname).toEqual('/jobs/9000/output');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should relaunch workflow job correctly', async () => {
|
||||||
|
WorkflowJobsAPI.readRelaunch.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
can_start_without_user_input: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/jobs/9000'],
|
||||||
|
});
|
||||||
|
WorkflowJobsAPI.relaunch.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 9000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<LaunchButton
|
||||||
|
resource={{
|
||||||
|
id: 1,
|
||||||
|
type: 'workflow_job',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{relaunchButton}
|
||||||
|
</LaunchButton>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: { history },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const button = wrapper.find('button');
|
||||||
|
button.prop('onClick')();
|
||||||
|
expect(WorkflowJobsAPI.readRelaunch).toHaveBeenCalledWith(1);
|
||||||
|
await sleep(0);
|
||||||
|
expect(WorkflowJobsAPI.relaunch).toHaveBeenCalledWith(1);
|
||||||
|
expect(history.location.pathname).toEqual('/jobs/9000/output');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should relaunch project sync correctly', async () => {
|
||||||
|
ProjectsAPI.readLaunchUpdate.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
can_start_without_user_input: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/jobs/9000'],
|
||||||
|
});
|
||||||
|
ProjectsAPI.launchUpdate.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 9000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<LaunchButton
|
||||||
|
resource={{
|
||||||
|
id: 1,
|
||||||
|
project: 5,
|
||||||
|
type: 'project_update',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{relaunchButton}
|
||||||
|
</LaunchButton>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: { history },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const button = wrapper.find('button');
|
||||||
|
button.prop('onClick')();
|
||||||
|
expect(ProjectsAPI.readLaunchUpdate).toHaveBeenCalledWith(5);
|
||||||
|
await sleep(0);
|
||||||
|
expect(ProjectsAPI.launchUpdate).toHaveBeenCalledWith(5);
|
||||||
|
expect(history.location.pathname).toEqual('/jobs/9000/output');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should relaunch project sync correctly', async () => {
|
||||||
|
InventorySourcesAPI.readLaunchUpdate.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
can_start_without_user_input: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/jobs/9000'],
|
||||||
|
});
|
||||||
|
InventorySourcesAPI.launchUpdate.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 9000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<LaunchButton
|
||||||
|
resource={{
|
||||||
|
id: 1,
|
||||||
|
inventory_source: 5,
|
||||||
|
type: 'inventory_update',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{relaunchButton}
|
||||||
|
</LaunchButton>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: { history },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const button = wrapper.find('button');
|
||||||
|
button.prop('onClick')();
|
||||||
|
expect(InventorySourcesAPI.readLaunchUpdate).toHaveBeenCalledWith(5);
|
||||||
|
await sleep(0);
|
||||||
|
expect(InventorySourcesAPI.launchUpdate).toHaveBeenCalledWith(5);
|
||||||
|
expect(history.location.pathname).toEqual('/jobs/9000/output');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('displays error modal after unsuccessful launch', async () => {
|
test('displays error modal after unsuccessful launch', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
<LaunchButton resource={resource}>{launchButton}</LaunchButton>
|
||||||
);
|
);
|
||||||
JobTemplatesAPI.launch.mockRejectedValue(
|
JobTemplatesAPI.launch.mockRejectedValue(
|
||||||
new Error({
|
new Error({
|
||||||
|
|||||||
@@ -19,17 +19,18 @@ function PromptModalForm({
|
|||||||
resource,
|
resource,
|
||||||
surveyConfig,
|
surveyConfig,
|
||||||
}) {
|
}) {
|
||||||
const { values, setTouched, validateForm } = useFormikContext();
|
const { setFieldTouched, values } = useFormikContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
steps,
|
steps,
|
||||||
isReady,
|
isReady,
|
||||||
|
validateStep,
|
||||||
visitStep,
|
visitStep,
|
||||||
visitAllSteps,
|
visitAllSteps,
|
||||||
contentError,
|
contentError,
|
||||||
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSubmit = () => {
|
||||||
const postValues = {};
|
const postValues = {};
|
||||||
const setValue = (key, value) => {
|
const setValue = (key, value) => {
|
||||||
if (typeof value !== 'undefined' && value !== null) {
|
if (typeof value !== 'undefined' && value !== null) {
|
||||||
@@ -37,6 +38,7 @@ function PromptModalForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const surveyValues = getSurveyValues(values);
|
const surveyValues = getSurveyValues(values);
|
||||||
|
setValue('credential_passwords', values.credential_passwords);
|
||||||
setValue('inventory_id', values.inventory?.id);
|
setValue('inventory_id', values.inventory?.id);
|
||||||
setValue(
|
setValue(
|
||||||
'credentials',
|
'credentials',
|
||||||
@@ -75,22 +77,25 @@ function PromptModalForm({
|
|||||||
<Wizard
|
<Wizard
|
||||||
isOpen
|
isOpen
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
onSave={handleSave}
|
onSave={handleSubmit}
|
||||||
|
onBack={async nextStep => {
|
||||||
|
validateStep(nextStep.id);
|
||||||
|
}}
|
||||||
onNext={async (nextStep, prevStep) => {
|
onNext={async (nextStep, prevStep) => {
|
||||||
if (nextStep.id === 'preview') {
|
if (nextStep.id === 'preview') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
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') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
visitStep(prevStep.prevId, setFieldTouched);
|
||||||
|
validateStep(nextStep.id);
|
||||||
}
|
}
|
||||||
await validateForm();
|
|
||||||
}}
|
}}
|
||||||
title={i18n._(t`Prompts`)}
|
title={i18n._(t`Prompts`)}
|
||||||
steps={
|
steps={
|
||||||
|
|||||||
@@ -82,8 +82,26 @@ describe('LaunchPrompt', () => {
|
|||||||
ask_credential_on_launch: true,
|
ask_credential_on_launch: true,
|
||||||
ask_scm_branch_on_launch: true,
|
ask_scm_branch_on_launch: true,
|
||||||
survey_enabled: true,
|
survey_enabled: true,
|
||||||
|
passwords_needed_to_start: ['ssh_password'],
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['ssh_password'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
resource={{
|
||||||
|
...resource,
|
||||||
|
summary_fields: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
resource={resource}
|
|
||||||
onLaunch={noop}
|
onLaunch={noop}
|
||||||
onCancel={noop}
|
onCancel={noop}
|
||||||
surveyConfig={{
|
surveyConfig={{
|
||||||
@@ -110,12 +128,13 @@ describe('LaunchPrompt', () => {
|
|||||||
const wizard = await waitForElement(wrapper, 'Wizard');
|
const wizard = await waitForElement(wrapper, 'Wizard');
|
||||||
const steps = wizard.prop('steps');
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(5);
|
expect(steps).toHaveLength(6);
|
||||||
expect(steps[0].name.props.children).toEqual('Inventory');
|
expect(steps[0].name.props.children).toEqual('Inventory');
|
||||||
expect(steps[1].name.props.children).toEqual('Credentials');
|
expect(steps[1].name.props.children).toEqual('Credentials');
|
||||||
expect(steps[2].name.props.children).toEqual('Other prompts');
|
expect(steps[2].name.props.children).toEqual('Credential passwords');
|
||||||
expect(steps[3].name.props.children).toEqual('Survey');
|
expect(steps[3].name.props.children).toEqual('Other prompts');
|
||||||
expect(steps[4].name.props.children).toEqual('Preview');
|
expect(steps[4].name.props.children).toEqual('Survey');
|
||||||
|
expect(steps[5].name.props.children).toEqual('Preview');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add inventory step', async () => {
|
test('should add inventory step', async () => {
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Form } from '@patternfly/react-core';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { PasswordField } from '../../FormField';
|
||||||
|
|
||||||
|
function CredentialPasswordsStep({ launchConfig, i18n }) {
|
||||||
|
const {
|
||||||
|
values: { credentials },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
const vaultsThatPrompt = [];
|
||||||
|
let showcredentialPasswordSsh = false;
|
||||||
|
let showcredentialPasswordPrivilegeEscalation = false;
|
||||||
|
let showcredentialPasswordPrivateKeyPassphrase = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
if (password === 'ssh_password') {
|
||||||
|
showcredentialPasswordSsh = true;
|
||||||
|
} else if (password === 'become_password') {
|
||||||
|
showcredentialPasswordPrivilegeEscalation = true;
|
||||||
|
} else if (password === 'ssh_key_unlock') {
|
||||||
|
showcredentialPasswordPrivateKeyPassphrase = true;
|
||||||
|
} else if (password.startsWith('vault_password')) {
|
||||||
|
const vaultId = password.split(/\.(.+)/)[1] || '';
|
||||||
|
vaultsThatPrompt.push(vaultId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (credentials) {
|
||||||
|
credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('ssh_password')
|
||||||
|
) {
|
||||||
|
showcredentialPasswordSsh = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('become_password')
|
||||||
|
) {
|
||||||
|
showcredentialPasswordPrivilegeEscalation = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('ssh_key_unlock')
|
||||||
|
) {
|
||||||
|
showcredentialPasswordPrivateKeyPassphrase = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultPasswordIds = launchConfigCredential.passwords_needed
|
||||||
|
.filter(passwordNeeded =>
|
||||||
|
passwordNeeded.startsWith('vault_password')
|
||||||
|
)
|
||||||
|
.map(vaultPassword => vaultPassword.split(/\.(.+)/)[1] || '');
|
||||||
|
|
||||||
|
vaultsThatPrompt.push(...vaultPasswordIds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (credential?.inputs?.password === 'ASK') {
|
||||||
|
showcredentialPasswordSsh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.become_password === 'ASK') {
|
||||||
|
showcredentialPasswordPrivilegeEscalation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.ssh_key_unlock === 'ASK') {
|
||||||
|
showcredentialPasswordPrivateKeyPassphrase = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.vault_password === 'ASK') {
|
||||||
|
vaultsThatPrompt.push(credential.inputs.vault_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
{showcredentialPasswordSsh && (
|
||||||
|
<PasswordField
|
||||||
|
id="launch-ssh-password"
|
||||||
|
label={i18n._(t`SSH password`)}
|
||||||
|
name="credential_passwords.ssh_password"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showcredentialPasswordPrivateKeyPassphrase && (
|
||||||
|
<PasswordField
|
||||||
|
id="launch-private-key-passphrase"
|
||||||
|
label={i18n._(t`Private key passphrase`)}
|
||||||
|
name="credential_passwords.ssh_key_unlock"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showcredentialPasswordPrivilegeEscalation && (
|
||||||
|
<PasswordField
|
||||||
|
id="launch-privilege-escalation-password"
|
||||||
|
label={i18n._(t`Privilege escalation password`)}
|
||||||
|
name="credential_passwords.become_password"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{vaultsThatPrompt.map(credId => (
|
||||||
|
<PasswordField
|
||||||
|
id={`launch-vault-password-${credId}`}
|
||||||
|
key={credId}
|
||||||
|
label={
|
||||||
|
credId === ''
|
||||||
|
? i18n._(t`Vault password`)
|
||||||
|
: i18n._(t`Vault password | ${credId}`)
|
||||||
|
}
|
||||||
|
name={`credential_passwords['vault_password${
|
||||||
|
credId !== '' ? `.${credId}` : ''
|
||||||
|
}']`}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(CredentialPasswordsStep);
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import CredentialPasswordsStep from './CredentialPasswordsStep';
|
||||||
|
|
||||||
|
describe('CredentialPasswordsStep', () => {
|
||||||
|
describe('JT default credentials (no credential replacement) and creds are promptable', () => {
|
||||||
|
test('should render ssh password field when JT has default machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['ssh_password'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render become password field when JT has default machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['become_password'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render private key passphrase field when JT has default machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
defaults: {
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['ssh_key_unlock'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render vault password field when JT has default vault cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['vault_password.1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-1')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
test('should render all password field when JT has default vault cred and machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: [
|
||||||
|
'ssh_password',
|
||||||
|
'become_password',
|
||||||
|
'ssh_key_unlock',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
passwords_needed: ['vault_password.1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-1')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Credentials have been replaced and creds are promptable', () => {
|
||||||
|
test('should render ssh password field when replacement machine cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: 'ASK',
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render become password field when replacement machine cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: 'ASK',
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render private key passphrase field when replacement machine cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: 'ASK',
|
||||||
|
vault_password: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render vault password field when replacement vault cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: 'ASK',
|
||||||
|
vault_id: 'foobar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
test('should render all password fields when replacement vault and machine creds prompt for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: 'ASK',
|
||||||
|
become_password: 'ASK',
|
||||||
|
ssh_key_unlock: 'ASK',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: 'ASK',
|
||||||
|
vault_id: 'foobar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Credentials have been replaced and creds are not promptable', () => {
|
||||||
|
test('should render ssh password field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['ssh_password'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render become password field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['become_password'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render private key passphrase field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['ssh_key_unlock'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render vault password field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['vault_password.foobar'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
test('should render all password fields when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: [
|
||||||
|
'ssh_password',
|
||||||
|
'become_password',
|
||||||
|
'ssh_key_unlock',
|
||||||
|
'vault_password.foobar',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 { 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';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
@@ -17,9 +18,10 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventoryStep({ i18n }) {
|
function InventoryStep({ i18n }) {
|
||||||
const [field, , helpers] = useField({
|
const [field, meta, helpers] = useField({
|
||||||
name: 'inventory',
|
name: 'inventory',
|
||||||
});
|
});
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -65,40 +67,45 @@ function InventoryStep({ i18n }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsList
|
<>
|
||||||
value={field.value ? [field.value] : []}
|
<OptionsList
|
||||||
options={inventories}
|
value={field.value ? [field.value] : []}
|
||||||
optionCount={count}
|
options={inventories}
|
||||||
searchColumns={[
|
optionCount={count}
|
||||||
{
|
searchColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'name__icontains',
|
name: i18n._(t`Name`),
|
||||||
isDefault: true,
|
key: 'name__icontains',
|
||||||
},
|
isDefault: true,
|
||||||
{
|
},
|
||||||
name: i18n._(t`Created By (Username)`),
|
{
|
||||||
key: 'created_by__username__icontains',
|
name: i18n._(t`Created By (Username)`),
|
||||||
},
|
key: 'created_by__username__icontains',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Modified By (Username)`),
|
{
|
||||||
key: 'modified_by__username__icontains',
|
name: i18n._(t`Modified By (Username)`),
|
||||||
},
|
key: 'modified_by__username__icontains',
|
||||||
]}
|
},
|
||||||
sortColumns={[
|
]}
|
||||||
{
|
sortColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'name',
|
name: i18n._(t`Name`),
|
||||||
},
|
key: 'name',
|
||||||
]}
|
},
|
||||||
searchableKeys={searchableKeys}
|
]}
|
||||||
relatedSearchableKeys={relatedSearchableKeys}
|
searchableKeys={searchableKeys}
|
||||||
header={i18n._(t`Inventory`)}
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
name="inventory"
|
header={i18n._(t`Inventory`)}
|
||||||
qsConfig={QS_CONFIG}
|
name="inventory"
|
||||||
readOnly
|
qsConfig={QS_CONFIG}
|
||||||
selectItem={helpers.setValue}
|
readOnly
|
||||||
deselectItem={() => field.onChange(null)}
|
selectItem={helpers.setValue}
|
||||||
/>
|
deselectItem={() => field.onChange(null)}
|
||||||
|
/>
|
||||||
|
{meta.touched && meta.error && (
|
||||||
|
<Alert variant="danger" isInline title={meta.error} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import CredentialPasswordsStep from './CredentialPasswordsStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
|
const STEP_ID = 'credentialPasswords';
|
||||||
|
|
||||||
|
const isValueMissing = val => {
|
||||||
|
return !val || val === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useCredentialPasswordsStep(
|
||||||
|
launchConfig,
|
||||||
|
i18n,
|
||||||
|
showStep,
|
||||||
|
visitedSteps
|
||||||
|
) {
|
||||||
|
const { values, setFieldError } = useFormikContext();
|
||||||
|
const hasError =
|
||||||
|
Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
|
checkForError(launchConfig, values);
|
||||||
|
|
||||||
|
return {
|
||||||
|
step: showStep
|
||||||
|
? {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: (
|
||||||
|
<StepName hasErrors={hasError} id="credential-passwords-step">
|
||||||
|
{i18n._(t`Credential passwords`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: (
|
||||||
|
<CredentialPasswordsStep launchConfig={launchConfig} i18n={i18n} />
|
||||||
|
),
|
||||||
|
enableNext: true,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
initialValues: getInitialValues(launchConfig, values.credentials),
|
||||||
|
isReady: true,
|
||||||
|
contentError: null,
|
||||||
|
hasError,
|
||||||
|
setTouched: setFieldTouched => {
|
||||||
|
Object.keys(values.credential_passwords).forEach(credentialValueKey =>
|
||||||
|
setFieldTouched(
|
||||||
|
`credential_passwords['${credentialValueKey}']`,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validate: () => {
|
||||||
|
const setPasswordFieldError = fieldName => {
|
||||||
|
setFieldError(fieldName, i18n._(t`This field may not be blank`));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
if (isValueMissing(values.credential_passwords[password])) {
|
||||||
|
setPasswordFieldError(`credential_passwords['${password}']`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (values.credentials) {
|
||||||
|
values.credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
launchConfigCredential.passwords_needed.forEach(password => {
|
||||||
|
if (isValueMissing(values.credential_passwords[password])) {
|
||||||
|
setPasswordFieldError(`credential_passwords['${password}']`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
credential?.inputs?.password === 'ASK' &&
|
||||||
|
isValueMissing(values.credential_passwords.ssh_password)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credential_passwords.ssh_password');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.become_password === 'ASK' &&
|
||||||
|
isValueMissing(values.credential_passwords.become_password)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credential_passwords.become_password');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.ssh_key_unlock === 'ASK' &&
|
||||||
|
isValueMissing(values.credential_passwords.ssh_key_unlock)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credential_passwords.ssh_key_unlock');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.vault_password === 'ASK' &&
|
||||||
|
isValueMissing(
|
||||||
|
values.credential_passwords[
|
||||||
|
`vault_password${
|
||||||
|
credential.inputs.vault_id !== ''
|
||||||
|
? `.${credential.inputs.vault_id}`
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError(
|
||||||
|
`credential_passwords['vault_password${
|
||||||
|
credential.inputs.vault_id !== ''
|
||||||
|
? `.${credential.inputs.vault_id}`
|
||||||
|
: ''
|
||||||
|
}']`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValues(launchConfig, selectedCredentials = []) {
|
||||||
|
const initialValues = {
|
||||||
|
credential_passwords: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!launchConfig) {
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
initialValues.credential_passwords[password] = '';
|
||||||
|
});
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCredentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
launchConfigCredential.passwords_needed.forEach(password => {
|
||||||
|
initialValues.credential_passwords[password] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (credential?.inputs?.password === 'ASK') {
|
||||||
|
initialValues.credential_passwords.ssh_password = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.become_password === 'ASK') {
|
||||||
|
initialValues.credential_passwords.become_password = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.ssh_key_unlock === 'ASK') {
|
||||||
|
initialValues.credential_passwords.ssh_key_unlock = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.vault_password === 'ASK') {
|
||||||
|
if (!credential.inputs.vault_id || credential.inputs.vault_id === '') {
|
||||||
|
initialValues.credential_passwords.vault_password = '';
|
||||||
|
} else {
|
||||||
|
initialValues.credential_passwords[
|
||||||
|
`vault_password.${credential.inputs.vault_id}`
|
||||||
|
] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForError(launchConfig, values) {
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
if (isValueMissing(values.credential_passwords[password])) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (values.credentials) {
|
||||||
|
values.credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
launchConfigCredential.passwords_needed.forEach(password => {
|
||||||
|
if (isValueMissing(values.credential_passwords[password])) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
credential?.inputs?.password === 'ASK' &&
|
||||||
|
isValueMissing(values.credential_passwords.ssh_password)
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.become_password === 'ASK' &&
|
||||||
|
isValueMissing(values.credential_passwords.become_password)
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.ssh_key_unlock === 'ASK' &&
|
||||||
|
isValueMissing(values.credential_passwords.ssh_key_unlock)
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.vault_password === 'ASK' &&
|
||||||
|
isValueMissing(
|
||||||
|
values.credential_passwords[
|
||||||
|
`vault_password${
|
||||||
|
credential.inputs.vault_id !== ''
|
||||||
|
? `.${credential.inputs.vault_id}`
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
@@ -9,15 +9,13 @@ export default function useCredentialsStep(launchConfig, resource, i18n) {
|
|||||||
return {
|
return {
|
||||||
step: getStep(launchConfig, i18n),
|
step: getStep(launchConfig, i18n),
|
||||||
initialValues: getInitialValues(launchConfig, resource),
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
validate: () => ({}),
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: null,
|
hasError: false,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('credentials', true, false);
|
||||||
credentials: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,27 @@ export default function useInventoryStep(
|
|||||||
i18n,
|
i18n,
|
||||||
visitedSteps
|
visitedSteps
|
||||||
) {
|
) {
|
||||||
const [, meta] = useField('inventory');
|
const [, meta, helpers] = useField('inventory');
|
||||||
const formError =
|
const formError =
|
||||||
Object.keys(visitedSteps).includes(STEP_ID) && (!meta.value || meta.error);
|
!resource || resource?.type === 'workflow_job_template'
|
||||||
|
? false
|
||||||
|
: Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
|
meta.touched &&
|
||||||
|
!meta.value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: getStep(launchConfig, i18n, formError),
|
step: getStep(launchConfig, i18n, formError),
|
||||||
initialValues: getInitialValues(launchConfig, resource),
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: launchConfig.ask_inventory_on_launch && formError,
|
hasError: launchConfig.ask_inventory_on_launch && formError,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('inventory', true, false);
|
||||||
inventory: true,
|
},
|
||||||
});
|
validate: () => {
|
||||||
|
if (meta.touched && !meta.value && resource.type === 'job_template') {
|
||||||
|
helpers.setError(i18n._(t`An inventory must be selected`));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,19 @@ export default function useOtherPromptsStep(launchConfig, resource, i18n) {
|
|||||||
initialValues: getInitialValues(launchConfig, resource),
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: null,
|
hasError: false,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
[
|
||||||
job_type: true,
|
'job_type',
|
||||||
limit: true,
|
'limit',
|
||||||
verbosity: true,
|
'verbosity',
|
||||||
diff_mode: true,
|
'diff_mode',
|
||||||
job_tags: true,
|
'job_tags',
|
||||||
skip_tags: true,
|
'skip_tags',
|
||||||
extra_vars: true,
|
'extra_vars',
|
||||||
});
|
].forEach(field => setFieldTouched(field, true, false));
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export default function usePreviewStep(
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
initialValues: {},
|
initialValues: {},
|
||||||
validate: () => ({}),
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
error: null,
|
error: null,
|
||||||
setTouched: () => {},
|
setTouched: () => {},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,89 +13,51 @@ export default function useSurveyStep(
|
|||||||
i18n,
|
i18n,
|
||||||
visitedSteps
|
visitedSteps
|
||||||
) {
|
) {
|
||||||
const { values } = useFormikContext();
|
const { setFieldError, values } = useFormikContext();
|
||||||
const errors = {};
|
const hasError =
|
||||||
const validate = () => {
|
Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
if (!launchConfig.survey_enabled || !surveyConfig?.spec) {
|
checkForError(launchConfig, surveyConfig, values);
|
||||||
return {};
|
|
||||||
}
|
|
||||||
surveyConfig.spec.forEach(question => {
|
|
||||||
const errMessage = validateField(
|
|
||||||
question,
|
|
||||||
values[`survey_${question.variable}`],
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
if (errMessage) {
|
|
||||||
errors[`survey_${question.variable}`] = errMessage;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
const formError = Object.keys(validate()).length > 0;
|
|
||||||
return {
|
return {
|
||||||
step: getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps),
|
step: launchConfig.survey_enabled
|
||||||
|
? {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: (
|
||||||
|
<StepName hasErrors={hasError} id="survey-step">
|
||||||
|
{i18n._(t`Survey`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: <SurveyStep surveyConfig={surveyConfig} i18n={i18n} />,
|
||||||
|
enableNext: true,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
initialValues: getInitialValues(launchConfig, surveyConfig, resource),
|
initialValues: getInitialValues(launchConfig, surveyConfig, resource),
|
||||||
validate,
|
|
||||||
surveyConfig,
|
surveyConfig,
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError,
|
hasError,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
if (!surveyConfig?.spec) {
|
if (!surveyConfig?.spec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fields = {};
|
|
||||||
surveyConfig.spec.forEach(question => {
|
surveyConfig.spec.forEach(question => {
|
||||||
fields[`survey_${question.variable}`] = true;
|
setFieldTouched(`survey_${question.variable}`, true, false);
|
||||||
});
|
});
|
||||||
setFieldsTouched(fields);
|
|
||||||
},
|
},
|
||||||
};
|
validate: () => {
|
||||||
}
|
if (launchConfig.survey_enabled && surveyConfig.spec) {
|
||||||
|
surveyConfig.spec.forEach(question => {
|
||||||
function validateField(question, value, i18n) {
|
const errMessage = validateSurveyField(
|
||||||
const isTextField = ['text', 'textarea'].includes(question.type);
|
question,
|
||||||
const isNumeric = ['integer', 'float'].includes(question.type);
|
values[`survey_${question.variable}`],
|
||||||
if (isTextField && (value || value === 0)) {
|
i18n
|
||||||
if (question.min && value.length < question.min) {
|
);
|
||||||
return i18n._(t`This field must be at least ${question.min} characters`);
|
if (errMessage) {
|
||||||
}
|
setFieldError(`survey_${question.variable}`, errMessage);
|
||||||
if (question.max && value.length > question.max) {
|
}
|
||||||
return i18n._(t`This field must not exceed ${question.max} characters`);
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
if (isNumeric && (value || value === 0)) {
|
|
||||||
if (value < question.min || value > question.max) {
|
|
||||||
return i18n._(
|
|
||||||
t`This field must be a number and have a value between ${question.min} and ${question.max}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (question.required && !value && value !== 0) {
|
|
||||||
return i18n._(t`This field must not be blank`);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
function getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps) {
|
|
||||||
if (!launchConfig.survey_enabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: STEP_ID,
|
|
||||||
name: (
|
|
||||||
<StepName
|
|
||||||
hasErrors={
|
|
||||||
Object.keys(visitedSteps).includes(STEP_ID) &&
|
|
||||||
Object.keys(validate()).length
|
|
||||||
}
|
|
||||||
id="survey-step"
|
|
||||||
>
|
|
||||||
{i18n._(t`Survey`)}
|
|
||||||
</StepName>
|
|
||||||
),
|
|
||||||
component: <SurveyStep surveyConfig={surveyConfig} i18n={i18n} />,
|
|
||||||
enableNext: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,3 +95,56 @@ function getInitialValues(launchConfig, surveyConfig, resource) {
|
|||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateSurveyField(question, value, i18n) {
|
||||||
|
const isTextField = ['text', 'textarea'].includes(question.type);
|
||||||
|
const isNumeric = ['integer', 'float'].includes(question.type);
|
||||||
|
if (isTextField && (value || value === 0)) {
|
||||||
|
if (question.min && value.length < question.min) {
|
||||||
|
return i18n._(t`This field must be at least ${question.min} characters`);
|
||||||
|
}
|
||||||
|
if (question.max && value.length > question.max) {
|
||||||
|
return i18n._(t`This field must not exceed ${question.max} characters`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNumeric && (value || value === 0)) {
|
||||||
|
if (value < question.min || value > question.max) {
|
||||||
|
return i18n._(
|
||||||
|
t`This field must be a number and have a value between ${question.min} and ${question.max}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (question.required && !value && value !== 0) {
|
||||||
|
return i18n._(t`This field must not be blank`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForError(launchConfig, surveyConfig, values) {
|
||||||
|
let hasError = false;
|
||||||
|
if (launchConfig.survey_enabled && surveyConfig.spec) {
|
||||||
|
surveyConfig.spec.forEach(question => {
|
||||||
|
const value = values[`survey_${question.variable}`];
|
||||||
|
const isTextField = ['text', 'textarea'].includes(question.type);
|
||||||
|
const isNumeric = ['integer', 'float'].includes(question.type);
|
||||||
|
if (isTextField && (value || value === 0)) {
|
||||||
|
if (
|
||||||
|
(question.min && value.length < question.min) ||
|
||||||
|
(question.max && value.length > question.max)
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNumeric && (value || value === 0)) {
|
||||||
|
if (value < question.min || value > question.max) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (question.required && !value && value !== 0) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,43 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import useInventoryStep from './steps/useInventoryStep';
|
import useInventoryStep from './steps/useInventoryStep';
|
||||||
import useCredentialsStep from './steps/useCredentialsStep';
|
import useCredentialsStep from './steps/useCredentialsStep';
|
||||||
|
import useCredentialPasswordsStep from './steps/useCredentialPasswordsStep';
|
||||||
import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
||||||
import useSurveyStep from './steps/useSurveyStep';
|
import useSurveyStep from './steps/useSurveyStep';
|
||||||
import usePreviewStep from './steps/usePreviewStep';
|
import usePreviewStep from './steps/usePreviewStep';
|
||||||
|
|
||||||
|
function showCredentialPasswordsStep(credentials = [], launchConfig) {
|
||||||
|
if (
|
||||||
|
!launchConfig?.ask_credential_on_launch &&
|
||||||
|
launchConfig?.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
return launchConfig.passwords_needed_to_start.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let credentialPasswordStepRequired = false;
|
||||||
|
|
||||||
|
credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
credentialPasswordStepRequired = true;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
credential?.inputs?.password === 'ASK' ||
|
||||||
|
credential?.inputs?.become_password === 'ASK' ||
|
||||||
|
credential?.inputs?.ssh_key_unlock === 'ASK' ||
|
||||||
|
credential?.inputs?.vault_password === 'ASK'
|
||||||
|
) {
|
||||||
|
credentialPasswordStepRequired = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return credentialPasswordStepRequired;
|
||||||
|
}
|
||||||
|
|
||||||
export default function useLaunchSteps(
|
export default function useLaunchSteps(
|
||||||
launchConfig,
|
launchConfig,
|
||||||
surveyConfig,
|
surveyConfig,
|
||||||
@@ -14,14 +47,21 @@ export default function useLaunchSteps(
|
|||||||
) {
|
) {
|
||||||
const [visited, setVisited] = useState({});
|
const [visited, setVisited] = useState({});
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
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, i18n),
|
||||||
|
useCredentialPasswordsStep(
|
||||||
|
launchConfig,
|
||||||
|
i18n,
|
||||||
|
showCredentialPasswordsStep(formikValues.credentials, launchConfig),
|
||||||
|
visited
|
||||||
|
),
|
||||||
useOtherPromptsStep(launchConfig, resource, i18n),
|
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||||
];
|
];
|
||||||
const { resetForm } = useFormikContext();
|
const { resetForm } = useFormikContext();
|
||||||
const hasErrors = steps.some(step => step.formError);
|
const hasErrors = steps.some(step => step.hasError);
|
||||||
|
|
||||||
steps.push(
|
steps.push(
|
||||||
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
|
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
|
||||||
@@ -38,16 +78,47 @@ export default function useLaunchSteps(
|
|||||||
...cur.initialValues,
|
...cur.initialValues,
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const newFormValues = { ...initialValues };
|
||||||
|
|
||||||
|
Object.keys(formikValues).forEach(formikValueKey => {
|
||||||
|
if (
|
||||||
|
formikValueKey === 'credential_passwords' &&
|
||||||
|
Object.prototype.hasOwnProperty.call(
|
||||||
|
newFormValues,
|
||||||
|
'credential_passwords'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const formikCredentialPasswords = formikValues.credential_passwords;
|
||||||
|
Object.keys(formikCredentialPasswords).forEach(
|
||||||
|
credentialPasswordValueKey => {
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(
|
||||||
|
newFormValues.credential_passwords,
|
||||||
|
credentialPasswordValueKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
newFormValues.credential_passwords[credentialPasswordValueKey] =
|
||||||
|
formikCredentialPasswords[credentialPasswordValueKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
Object.prototype.hasOwnProperty.call(newFormValues, formikValueKey)
|
||||||
|
) {
|
||||||
|
newFormValues[formikValueKey] = formikValues[formikValueKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
resetForm({
|
resetForm({
|
||||||
values: {
|
values: newFormValues,
|
||||||
...initialValues,
|
touched,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [stepsAreReady]);
|
}, [formikValues.credentials, stepsAreReady]);
|
||||||
|
|
||||||
const stepWithError = steps.find(s => s.contentError);
|
const stepWithError = steps.find(s => s.contentError);
|
||||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||||
@@ -55,20 +126,26 @@ export default function useLaunchSteps(
|
|||||||
return {
|
return {
|
||||||
steps: pfSteps,
|
steps: pfSteps,
|
||||||
isReady,
|
isReady,
|
||||||
visitStep: stepId =>
|
validateStep: stepId => {
|
||||||
|
steps.find(s => s?.step?.id === stepId).validate();
|
||||||
|
},
|
||||||
|
visitStep: (prevStepId, setFieldTouched) => {
|
||||||
setVisited({
|
setVisited({
|
||||||
...visited,
|
...visited,
|
||||||
[stepId]: true,
|
[prevStepId]: true,
|
||||||
}),
|
});
|
||||||
visitAllSteps: setFieldsTouched => {
|
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||||
|
},
|
||||||
|
visitAllSteps: setFieldTouched => {
|
||||||
setVisited({
|
setVisited({
|
||||||
inventory: true,
|
inventory: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
credentialPasswords: true,
|
||||||
other: true,
|
other: true,
|
||||||
survey: true,
|
survey: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
});
|
});
|
||||||
steps.forEach(s => s.setTouched(setFieldsTouched));
|
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||||
},
|
},
|
||||||
contentError,
|
contentError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { t } from '@lingui/macro';
|
|||||||
import useRequest from '../../util/useRequest';
|
import useRequest from '../../util/useRequest';
|
||||||
import { UnifiedJobsAPI } from '../../api';
|
import { UnifiedJobsAPI } from '../../api';
|
||||||
import ContentError from '../../components/ContentError';
|
import ContentError from '../../components/ContentError';
|
||||||
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
||||||
|
|
||||||
const NOT_FOUND = 'not found';
|
const NOT_FOUND = 'not found';
|
||||||
@@ -46,8 +47,13 @@ function JobTypeRedirect({ id, path, view, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isLoading || !job?.id) {
|
if (isLoading || !job?.id) {
|
||||||
// TODO show loading state
|
return (
|
||||||
return <div>Loading...</div>;
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentLoading />
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const type = JOB_TYPE_URL_SEGMENTS[job.type];
|
const type = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||||
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;
|
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function NodeModalForm({
|
|||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
const { values, setTouched, validateForm } = useFormikContext();
|
const { values, setFieldTouched } = useFormikContext();
|
||||||
|
|
||||||
const [triggerNext, setTriggerNext] = useState(0);
|
const [triggerNext, setTriggerNext] = useState(0);
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ function NodeModalForm({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
steps: promptSteps,
|
steps: promptSteps,
|
||||||
|
validateStep,
|
||||||
visitStep,
|
visitStep,
|
||||||
visitAllSteps,
|
visitAllSteps,
|
||||||
contentError,
|
contentError,
|
||||||
@@ -192,24 +193,27 @@ function NodeModalForm({
|
|||||||
onSave={() => {
|
onSave={() => {
|
||||||
handleSaveNode();
|
handleSaveNode();
|
||||||
}}
|
}}
|
||||||
|
onBack={async nextStep => {
|
||||||
|
validateStep(nextStep.id);
|
||||||
|
}}
|
||||||
onGoToStep={async (nextStep, prevStep) => {
|
onGoToStep={async (nextStep, prevStep) => {
|
||||||
if (nextStep.id === 'preview') {
|
if (nextStep.id === 'preview') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
visitStep(prevStep.prevId, setFieldTouched);
|
||||||
|
validateStep(nextStep.id);
|
||||||
}
|
}
|
||||||
await validateForm();
|
|
||||||
}}
|
}}
|
||||||
steps={promptSteps}
|
steps={promptSteps}
|
||||||
css="overflow: scroll"
|
css="overflow: scroll"
|
||||||
title={title}
|
title={title}
|
||||||
onNext={async (nextStep, prevStep) => {
|
onNext={async (nextStep, prevStep) => {
|
||||||
if (nextStep.id === 'preview') {
|
if (nextStep.id === 'preview') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
visitStep(prevStep.prevId, setFieldTouched);
|
||||||
|
validateStep(nextStep.id);
|
||||||
}
|
}
|
||||||
await validateForm();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ export default function useNodeTypeStep(i18n) {
|
|||||||
initialValues: getInitialValues(),
|
initialValues: getInitialValues(),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: meta.error,
|
hasError: !!meta.error,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('nodeType', true, false);
|
||||||
inventory: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ export default function useRunTypeStep(i18n, askLinkType) {
|
|||||||
initialValues: askLinkType ? { linkType: 'success' } : {},
|
initialValues: askLinkType ? { linkType: 'success' } : {},
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: meta.error,
|
hasError: !!meta.error,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('linkType', true, false);
|
||||||
inventory: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getStep(askLinkType, meta, i18n) {
|
function getStep(askLinkType, meta, i18n) {
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ export default function useWorkflowNodeSteps(
|
|||||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasErrors = steps.some(step => step.formError);
|
const hasErrors = steps.some(step => step.hasError);
|
||||||
|
|
||||||
steps.push(
|
steps.push(
|
||||||
usePreviewStep(
|
usePreviewStep(
|
||||||
launchConfig,
|
launchConfig,
|
||||||
@@ -250,12 +251,17 @@ export default function useWorkflowNodeSteps(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
steps: pfSteps,
|
steps: pfSteps,
|
||||||
visitStep: stepId =>
|
validateStep: stepId => {
|
||||||
|
steps.find(s => s?.step?.id === stepId).validate();
|
||||||
|
},
|
||||||
|
visitStep: (prevStepId, setFieldTouched) => {
|
||||||
setVisited({
|
setVisited({
|
||||||
...visited,
|
...visited,
|
||||||
[stepId]: true,
|
[prevStepId]: true,
|
||||||
}),
|
});
|
||||||
visitAllSteps: setFieldsTouched => {
|
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||||
|
},
|
||||||
|
visitAllSteps: setFieldTouched => {
|
||||||
setVisited({
|
setVisited({
|
||||||
inventory: true,
|
inventory: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
@@ -263,7 +269,7 @@ export default function useWorkflowNodeSteps(
|
|||||||
survey: true,
|
survey: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
});
|
});
|
||||||
steps.forEach(s => s.setTouched(setFieldsTouched));
|
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||||
},
|
},
|
||||||
contentError,
|
contentError,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user