mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03: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:
commit
8a7c714613
@ -12,7 +12,15 @@ import {
|
||||
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
|
||||
|
||||
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 [field, meta] = useField({ name, validate });
|
||||
|
||||
@ -38,6 +46,7 @@ function PasswordInput(props) {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<TextInput
|
||||
autoComplete={autocomplete}
|
||||
id={id}
|
||||
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
|
||||
{...field}
|
||||
@ -55,6 +64,7 @@ function PasswordInput(props) {
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
autocomplete: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
validate: PropTypes.func,
|
||||
@ -63,6 +73,7 @@ PasswordInput.propTypes = {
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
autocomplete: 'new-password',
|
||||
validate: () => {},
|
||||
isRequired: false,
|
||||
isDisabled: false,
|
||||
|
||||
@ -25,6 +25,8 @@ function canLaunchWithoutPrompt(launchData) {
|
||||
!launchData.ask_limit_on_launch &&
|
||||
!launchData.ask_scm_branch_on_launch &&
|
||||
!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.length === 0)
|
||||
);
|
||||
@ -100,17 +102,20 @@ class LaunchButton extends React.Component {
|
||||
async launchWithParams(params) {
|
||||
try {
|
||||
const { history, resource } = this.props;
|
||||
const jobPromise =
|
||||
resource.type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.launch(resource.id, params || {})
|
||||
: JobTemplatesAPI.launch(resource.id, params || {});
|
||||
let jobPromise;
|
||||
|
||||
if (resource.type === 'job_template') {
|
||||
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;
|
||||
history.push(
|
||||
`/${
|
||||
resource.type === 'workflow_job_template' ? 'jobs/workflow' : 'jobs'
|
||||
}/${job.id}/output`
|
||||
);
|
||||
history.push(`/jobs/${job.id}/output`);
|
||||
} catch (launchError) {
|
||||
this.setState({ launchError });
|
||||
}
|
||||
@ -127,20 +132,15 @@ class LaunchButton extends React.Component {
|
||||
readRelaunch = InventorySourcesAPI.readLaunchUpdate(
|
||||
resource.inventory_source
|
||||
);
|
||||
relaunch = InventorySourcesAPI.launchUpdate(resource.inventory_source);
|
||||
} else if (resource.type === 'project_update') {
|
||||
// We'll need to handle the scenario where the project no longer exists
|
||||
readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project);
|
||||
relaunch = ProjectsAPI.launchUpdate(resource.project);
|
||||
} else if (resource.type === 'workflow_job') {
|
||||
readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id);
|
||||
relaunch = WorkflowJobsAPI.relaunch(resource.id);
|
||||
} else if (resource.type === 'ad_hoc_command') {
|
||||
readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id);
|
||||
relaunch = AdHocCommandsAPI.relaunch(resource.id);
|
||||
} else if (resource.type === 'job') {
|
||||
readRelaunch = JobsAPI.readRelaunch(resource.id);
|
||||
relaunch = JobsAPI.relaunch(resource.id);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -149,11 +149,22 @@ class LaunchButton extends React.Component {
|
||||
!relaunchConfig.passwords_needed_to_start ||
|
||||
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;
|
||||
history.push(`/jobs/${job.id}/output`);
|
||||
} else {
|
||||
// TODO: restructure (async?) to send launch command after prompts
|
||||
// TODO: does relaunch need different prompt treatment than launch?
|
||||
this.setState({
|
||||
showLaunchPrompt: true,
|
||||
launchConfig: relaunchConfig,
|
||||
|
||||
@ -4,10 +4,16 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import { sleep } from '../../../testUtils/testUtils';
|
||||
|
||||
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/models/JobTemplates');
|
||||
jest.mock('../../api');
|
||||
|
||||
describe('LaunchButton', () => {
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
@ -22,10 +28,14 @@ describe('LaunchButton', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const children = ({ handleLaunch }) => (
|
||||
const launchButton = ({ handleLaunch }) => (
|
||||
<button type="submit" onClick={() => handleLaunch()} />
|
||||
);
|
||||
|
||||
const relaunchButton = ({ handleRelaunch }) => (
|
||||
<button type="submit" onClick={() => handleRelaunch()} />
|
||||
);
|
||||
|
||||
const resource = {
|
||||
id: 1,
|
||||
type: 'job_template',
|
||||
@ -35,7 +45,7 @@ describe('LaunchButton', () => {
|
||||
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||
<LaunchButton resource={resource}>{launchButton}</LaunchButton>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
@ -51,7 +61,7 @@ describe('LaunchButton', () => {
|
||||
},
|
||||
});
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>,
|
||||
<LaunchButton resource={resource}>{launchButton}</LaunchButton>,
|
||||
{
|
||||
context: {
|
||||
router: { history },
|
||||
@ -87,7 +97,7 @@ describe('LaunchButton', () => {
|
||||
type: 'workflow_job_template',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{launchButton}
|
||||
</LaunchButton>,
|
||||
{
|
||||
context: {
|
||||
@ -100,12 +110,162 @@ describe('LaunchButton', () => {
|
||||
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
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 () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||
<LaunchButton resource={resource}>{launchButton}</LaunchButton>
|
||||
);
|
||||
JobTemplatesAPI.launch.mockRejectedValue(
|
||||
new Error({
|
||||
|
||||
@ -19,17 +19,18 @@ function PromptModalForm({
|
||||
resource,
|
||||
surveyConfig,
|
||||
}) {
|
||||
const { values, setTouched, validateForm } = useFormikContext();
|
||||
const { setFieldTouched, values } = useFormikContext();
|
||||
|
||||
const {
|
||||
steps,
|
||||
isReady,
|
||||
validateStep,
|
||||
visitStep,
|
||||
visitAllSteps,
|
||||
contentError,
|
||||
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSubmit = () => {
|
||||
const postValues = {};
|
||||
const setValue = (key, value) => {
|
||||
if (typeof value !== 'undefined' && value !== null) {
|
||||
@ -37,6 +38,7 @@ function PromptModalForm({
|
||||
}
|
||||
};
|
||||
const surveyValues = getSurveyValues(values);
|
||||
setValue('credential_passwords', values.credential_passwords);
|
||||
setValue('inventory_id', values.inventory?.id);
|
||||
setValue(
|
||||
'credentials',
|
||||
@ -75,22 +77,25 @@ function PromptModalForm({
|
||||
<Wizard
|
||||
isOpen
|
||||
onClose={onCancel}
|
||||
onSave={handleSave}
|
||||
onSave={handleSubmit}
|
||||
onBack={async nextStep => {
|
||||
validateStep(nextStep.id);
|
||||
}}
|
||||
onNext={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setTouched);
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId);
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
onGoToStep={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setTouched);
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId);
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
title={i18n._(t`Prompts`)}
|
||||
steps={
|
||||
|
||||
@ -82,8 +82,26 @@ describe('LaunchPrompt', () => {
|
||||
ask_credential_on_launch: true,
|
||||
ask_scm_branch_on_launch: 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}
|
||||
onCancel={noop}
|
||||
surveyConfig={{
|
||||
@ -110,12 +128,13 @@ describe('LaunchPrompt', () => {
|
||||
const wizard = await waitForElement(wrapper, 'Wizard');
|
||||
const steps = wizard.prop('steps');
|
||||
|
||||
expect(steps).toHaveLength(5);
|
||||
expect(steps).toHaveLength(6);
|
||||
expect(steps[0].name.props.children).toEqual('Inventory');
|
||||
expect(steps[1].name.props.children).toEqual('Credentials');
|
||||
expect(steps[2].name.props.children).toEqual('Other prompts');
|
||||
expect(steps[3].name.props.children).toEqual('Survey');
|
||||
expect(steps[4].name.props.children).toEqual('Preview');
|
||||
expect(steps[2].name.props.children).toEqual('Credential passwords');
|
||||
expect(steps[3].name.props.children).toEqual('Other prompts');
|
||||
expect(steps[4].name.props.children).toEqual('Survey');
|
||||
expect(steps[5].name.props.children).toEqual('Preview');
|
||||
});
|
||||
|
||||
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 { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
@ -17,9 +18,10 @@ const QS_CONFIG = getQSConfig('inventory', {
|
||||
});
|
||||
|
||||
function InventoryStep({ i18n }) {
|
||||
const [field, , helpers] = useField({
|
||||
const [field, meta, helpers] = useField({
|
||||
name: 'inventory',
|
||||
});
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
@ -65,40 +67,45 @@ function InventoryStep({ i18n }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsList
|
||||
value={field.value ? [field.value] : []}
|
||||
options={inventories}
|
||||
optionCount={count}
|
||||
searchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username__icontains',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username__icontains',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
searchableKeys={searchableKeys}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
header={i18n._(t`Inventory`)}
|
||||
name="inventory"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly
|
||||
selectItem={helpers.setValue}
|
||||
deselectItem={() => field.onChange(null)}
|
||||
/>
|
||||
<>
|
||||
<OptionsList
|
||||
value={field.value ? [field.value] : []}
|
||||
options={inventories}
|
||||
optionCount={count}
|
||||
searchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username__icontains',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username__icontains',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
searchableKeys={searchableKeys}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
header={i18n._(t`Inventory`)}
|
||||
name="inventory"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly
|
||||
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 {
|
||||
step: getStep(launchConfig, i18n),
|
||||
initialValues: getInitialValues(launchConfig, resource),
|
||||
validate: () => ({}),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: null,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
credentials: true,
|
||||
});
|
||||
hasError: false,
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('credentials', true, false);
|
||||
},
|
||||
validate: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -12,20 +12,27 @@ export default function useInventoryStep(
|
||||
i18n,
|
||||
visitedSteps
|
||||
) {
|
||||
const [, meta] = useField('inventory');
|
||||
const [, meta, helpers] = useField('inventory');
|
||||
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 {
|
||||
step: getStep(launchConfig, i18n, formError),
|
||||
initialValues: getInitialValues(launchConfig, resource),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: launchConfig.ask_inventory_on_launch && formError,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
inventory: true,
|
||||
});
|
||||
hasError: launchConfig.ask_inventory_on_launch && formError,
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('inventory', true, false);
|
||||
},
|
||||
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),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: null,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
job_type: true,
|
||||
limit: true,
|
||||
verbosity: true,
|
||||
diff_mode: true,
|
||||
job_tags: true,
|
||||
skip_tags: true,
|
||||
extra_vars: true,
|
||||
});
|
||||
hasError: false,
|
||||
setTouched: setFieldTouched => {
|
||||
[
|
||||
'job_type',
|
||||
'limit',
|
||||
'verbosity',
|
||||
'diff_mode',
|
||||
'job_tags',
|
||||
'skip_tags',
|
||||
'extra_vars',
|
||||
].forEach(field => setFieldTouched(field, true, false));
|
||||
},
|
||||
validate: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -35,9 +35,9 @@ export default function usePreviewStep(
|
||||
}
|
||||
: null,
|
||||
initialValues: {},
|
||||
validate: () => ({}),
|
||||
isReady: true,
|
||||
error: null,
|
||||
setTouched: () => {},
|
||||
validate: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -13,89 +13,51 @@ export default function useSurveyStep(
|
||||
i18n,
|
||||
visitedSteps
|
||||
) {
|
||||
const { values } = useFormikContext();
|
||||
const errors = {};
|
||||
const validate = () => {
|
||||
if (!launchConfig.survey_enabled || !surveyConfig?.spec) {
|
||||
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;
|
||||
const { setFieldError, values } = useFormikContext();
|
||||
const hasError =
|
||||
Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||
checkForError(launchConfig, surveyConfig, values);
|
||||
|
||||
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),
|
||||
validate,
|
||||
surveyConfig,
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError,
|
||||
setTouched: setFieldsTouched => {
|
||||
hasError,
|
||||
setTouched: setFieldTouched => {
|
||||
if (!surveyConfig?.spec) {
|
||||
return;
|
||||
}
|
||||
const fields = {};
|
||||
surveyConfig.spec.forEach(question => {
|
||||
fields[`survey_${question.variable}`] = true;
|
||||
setFieldTouched(`survey_${question.variable}`, true, false);
|
||||
});
|
||||
setFieldsTouched(fields);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function validateField(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 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,
|
||||
validate: () => {
|
||||
if (launchConfig.survey_enabled && surveyConfig.spec) {
|
||||
surveyConfig.spec.forEach(question => {
|
||||
const errMessage = validateSurveyField(
|
||||
question,
|
||||
values[`survey_${question.variable}`],
|
||||
i18n
|
||||
);
|
||||
if (errMessage) {
|
||||
setFieldError(`survey_${question.variable}`, errMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -133,3 +95,56 @@ function getInitialValues(launchConfig, surveyConfig, resource) {
|
||||
|
||||
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 useInventoryStep from './steps/useInventoryStep';
|
||||
import useCredentialsStep from './steps/useCredentialsStep';
|
||||
import useCredentialPasswordsStep from './steps/useCredentialPasswordsStep';
|
||||
import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
||||
import useSurveyStep from './steps/useSurveyStep';
|
||||
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(
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
@ -14,14 +47,21 @@ export default function useLaunchSteps(
|
||||
) {
|
||||
const [visited, setVisited] = useState({});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const { touched, values: formikValues } = useFormikContext();
|
||||
const steps = [
|
||||
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||
useCredentialsStep(launchConfig, resource, i18n),
|
||||
useCredentialPasswordsStep(
|
||||
launchConfig,
|
||||
i18n,
|
||||
showCredentialPasswordsStep(formikValues.credentials, launchConfig),
|
||||
visited
|
||||
),
|
||||
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||
];
|
||||
const { resetForm } = useFormikContext();
|
||||
const hasErrors = steps.some(step => step.formError);
|
||||
const hasErrors = steps.some(step => step.hasError);
|
||||
|
||||
steps.push(
|
||||
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
|
||||
@ -38,16 +78,47 @@ export default function useLaunchSteps(
|
||||
...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({
|
||||
values: {
|
||||
...initialValues,
|
||||
},
|
||||
values: newFormValues,
|
||||
touched,
|
||||
});
|
||||
|
||||
setIsReady(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stepsAreReady]);
|
||||
}, [formikValues.credentials, stepsAreReady]);
|
||||
|
||||
const stepWithError = steps.find(s => s.contentError);
|
||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||
@ -55,20 +126,26 @@ export default function useLaunchSteps(
|
||||
return {
|
||||
steps: pfSteps,
|
||||
isReady,
|
||||
visitStep: stepId =>
|
||||
validateStep: stepId => {
|
||||
steps.find(s => s?.step?.id === stepId).validate();
|
||||
},
|
||||
visitStep: (prevStepId, setFieldTouched) => {
|
||||
setVisited({
|
||||
...visited,
|
||||
[stepId]: true,
|
||||
}),
|
||||
visitAllSteps: setFieldsTouched => {
|
||||
[prevStepId]: true,
|
||||
});
|
||||
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||
},
|
||||
visitAllSteps: setFieldTouched => {
|
||||
setVisited({
|
||||
inventory: true,
|
||||
credentials: true,
|
||||
credentialPasswords: true,
|
||||
other: true,
|
||||
survey: true,
|
||||
preview: true,
|
||||
});
|
||||
steps.forEach(s => s.setTouched(setFieldsTouched));
|
||||
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||
},
|
||||
contentError,
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import { t } from '@lingui/macro';
|
||||
import useRequest from '../../util/useRequest';
|
||||
import { UnifiedJobsAPI } from '../../api';
|
||||
import ContentError from '../../components/ContentError';
|
||||
import ContentLoading from '../../components/ContentLoading';
|
||||
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
||||
|
||||
const NOT_FOUND = 'not found';
|
||||
@ -46,8 +47,13 @@ function JobTypeRedirect({ id, path, view, i18n }) {
|
||||
);
|
||||
}
|
||||
if (isLoading || !job?.id) {
|
||||
// TODO show loading state
|
||||
return <div>Loading...</div>;
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<ContentLoading />
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
const type = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;
|
||||
|
||||
@ -44,7 +44,7 @@ function NodeModalForm({
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { values, setTouched, validateForm } = useFormikContext();
|
||||
const { values, setFieldTouched } = useFormikContext();
|
||||
|
||||
const [triggerNext, setTriggerNext] = useState(0);
|
||||
|
||||
@ -60,6 +60,7 @@ function NodeModalForm({
|
||||
|
||||
const {
|
||||
steps: promptSteps,
|
||||
validateStep,
|
||||
visitStep,
|
||||
visitAllSteps,
|
||||
contentError,
|
||||
@ -192,24 +193,27 @@ function NodeModalForm({
|
||||
onSave={() => {
|
||||
handleSaveNode();
|
||||
}}
|
||||
onBack={async nextStep => {
|
||||
validateStep(nextStep.id);
|
||||
}}
|
||||
onGoToStep={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setTouched);
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId);
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
steps={promptSteps}
|
||||
css="overflow: scroll"
|
||||
title={title}
|
||||
onNext={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setTouched);
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId);
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -17,12 +17,11 @@ export default function useNodeTypeStep(i18n) {
|
||||
initialValues: getInitialValues(),
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: meta.error,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
inventory: true,
|
||||
});
|
||||
hasError: !!meta.error,
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('nodeType', true, false);
|
||||
},
|
||||
validate: () => {},
|
||||
};
|
||||
}
|
||||
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||
|
||||
@ -14,12 +14,11 @@ export default function useRunTypeStep(i18n, askLinkType) {
|
||||
initialValues: askLinkType ? { linkType: 'success' } : {},
|
||||
isReady: true,
|
||||
contentError: null,
|
||||
formError: meta.error,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
inventory: true,
|
||||
});
|
||||
hasError: !!meta.error,
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('linkType', true, false);
|
||||
},
|
||||
validate: () => {},
|
||||
};
|
||||
}
|
||||
function getStep(askLinkType, meta, i18n) {
|
||||
|
||||
@ -194,7 +194,8 @@ export default function useWorkflowNodeSteps(
|
||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||
];
|
||||
|
||||
const hasErrors = steps.some(step => step.formError);
|
||||
const hasErrors = steps.some(step => step.hasError);
|
||||
|
||||
steps.push(
|
||||
usePreviewStep(
|
||||
launchConfig,
|
||||
@ -250,12 +251,17 @@ export default function useWorkflowNodeSteps(
|
||||
|
||||
return {
|
||||
steps: pfSteps,
|
||||
visitStep: stepId =>
|
||||
validateStep: stepId => {
|
||||
steps.find(s => s?.step?.id === stepId).validate();
|
||||
},
|
||||
visitStep: (prevStepId, setFieldTouched) => {
|
||||
setVisited({
|
||||
...visited,
|
||||
[stepId]: true,
|
||||
}),
|
||||
visitAllSteps: setFieldsTouched => {
|
||||
[prevStepId]: true,
|
||||
});
|
||||
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||
},
|
||||
visitAllSteps: setFieldTouched => {
|
||||
setVisited({
|
||||
inventory: true,
|
||||
credentials: true,
|
||||
@ -263,7 +269,7 @@ export default function useWorkflowNodeSteps(
|
||||
survey: true,
|
||||
preview: true,
|
||||
});
|
||||
steps.forEach(s => s.setTouched(setFieldsTouched));
|
||||
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||
},
|
||||
contentError,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user