mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Merge pull request #10576 from AlexSCorey/9399-revampAdHocCommandsWorkflow
Adds Ad Hoc Preview step and adds workflow similar to prompt on launch SUMMARY This closes #9399 it also introduce the same workflow that prompt on launch uses in the Ad Hoc Commands, where the user to go between steps as they wish. ISSUE TYPE Feature Pull Request COMPONENT NAME UI AWX VERSION ADDITIONAL INFORMATION Reviewed-by: Kersom <None> Reviewed-by: Jake McDermott <yo@jakemcdermott.me> Reviewed-by: Sarah Akus <sakus@redhat.com>
This commit is contained in:
commit
8183179850
@ -184,14 +184,6 @@ describe('<AdHocCommands />', () => {
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine credential"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
@ -247,6 +239,12 @@ describe('<AdHocCommands />', () => {
|
||||
wrapper.find('CheckboxListItem[label="Cred 4"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
// fourth step
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
@ -353,13 +351,6 @@ describe('<AdHocCommands />', () => {
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine credential"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
@ -423,7 +414,13 @@ describe('<AdHocCommands />', () => {
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
// fourth step of wizard
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
await waitForElement(wrapper, 'ErrorDetail', el => el.length > 0);
|
||||
});
|
||||
|
||||
|
||||
@ -1,26 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
|
||||
import { Tooltip } from '@patternfly/react-core';
|
||||
import { withFormik, useFormikContext } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import Wizard from '../Wizard';
|
||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||
import AdHocDetailsStep from './AdHocDetailsStep';
|
||||
import AdHocExecutionEnvironmentStep from './AdHocExecutionEnvironmentStep';
|
||||
|
||||
const AlertText = styled.div`
|
||||
color: var(--pf-global--danger-color--200);
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
`;
|
||||
|
||||
const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
||||
margin-left: 10px;
|
||||
color: var(--pf-global--danger-color--100);
|
||||
`;
|
||||
import useAdHocLaunchSteps from './useAdHocLaunchSteps';
|
||||
|
||||
function AdHocCommandsWizard({
|
||||
onLaunch,
|
||||
@ -30,100 +14,44 @@ function AdHocCommandsWizard({
|
||||
credentialTypeId,
|
||||
organizationId,
|
||||
}) {
|
||||
const [currentStepId, setCurrentStepId] = useState(1);
|
||||
const [enableLaunch, setEnableLaunch] = useState(false);
|
||||
const { setFieldTouched, values } = useFormikContext();
|
||||
|
||||
const { values, errors, touched } = useFormikContext();
|
||||
|
||||
const enabledNextOnDetailsStep = () => {
|
||||
if (!values.module_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (values.module_name === 'shell' || values.module_name === 'command') {
|
||||
if (values.module_args) {
|
||||
return true;
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return undefined; // makes the linter happy;
|
||||
};
|
||||
const hasDetailsStepError = errors.module_args && touched.module_args;
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
key: 1,
|
||||
name: hasDetailsStepError ? (
|
||||
<AlertText>
|
||||
{t`Details`}
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={t`This step contains errors`}
|
||||
trigger="click mouseenter focus"
|
||||
>
|
||||
<ExclamationCircleIcon />
|
||||
</Tooltip>
|
||||
</AlertText>
|
||||
) : (
|
||||
t`Details`
|
||||
),
|
||||
component: (
|
||||
<AdHocDetailsStep
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
/>
|
||||
),
|
||||
enableNext: enabledNextOnDetailsStep(),
|
||||
nextButtonText: t`Next`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
key: 2,
|
||||
name: t`Execution Environment`,
|
||||
component: (
|
||||
<AdHocExecutionEnvironmentStep organizationId={organizationId} />
|
||||
),
|
||||
// Removed this line when https://github.com/patternfly/patternfly-react/issues/5729 is fixed
|
||||
stepNavItemProps: { style: { whiteSpace: 'nowrap' } },
|
||||
enableNext: true,
|
||||
nextButtonText: t`Next`,
|
||||
canJumpTo: currentStepId >= 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
key: 3,
|
||||
name: t`Machine credential`,
|
||||
component: (
|
||||
<AdHocCredentialStep
|
||||
credentialTypeId={credentialTypeId}
|
||||
onEnableLaunch={() => setEnableLaunch(true)}
|
||||
/>
|
||||
),
|
||||
enableNext: enableLaunch && Object.values(errors).length === 0,
|
||||
nextButtonText: t`Launch`,
|
||||
canJumpTo: currentStepId >= 2,
|
||||
},
|
||||
];
|
||||
|
||||
const currentStep = steps.find(step => step.id === currentStepId);
|
||||
const { steps, validateStep, visitStep, visitAllSteps } = useAdHocLaunchSteps(
|
||||
moduleOptions,
|
||||
verbosityOptions,
|
||||
organizationId,
|
||||
credentialTypeId
|
||||
);
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
style={{ overflow: 'scroll' }}
|
||||
isOpen
|
||||
onNext={step => setCurrentStepId(step.id)}
|
||||
onNext={(nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
}}
|
||||
onClose={() => onCloseWizard()}
|
||||
onSave={() => {
|
||||
onLaunch(values);
|
||||
}}
|
||||
onGoToStep={(nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
}}
|
||||
steps={steps}
|
||||
title={t`Run command`}
|
||||
nextButtonText={currentStep.nextButtonText || undefined}
|
||||
backButtonText={t`Back`}
|
||||
cancelButtonText={t`Cancel`}
|
||||
nextButtonText={t`Next`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -60,50 +60,29 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
||||
});
|
||||
|
||||
test('next and nav item should be disabled', async () => {
|
||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
expect(
|
||||
wrapper.find('WizardNavItem[content="Details"]').prop('isCurrent')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('WizardNavItem[content="Details"]').prop('isDisabled')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine credential"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine credential"]')
|
||||
.prop('isCurrent')
|
||||
).toBe(false);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
});
|
||||
test('launch button should be disabled', async () => {
|
||||
waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
|
||||
test('next button should become active, and should navigate to the next step', async () => {
|
||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#module_args').simulate('change', {
|
||||
target: { value: 'foo', name: 'module_args' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.update();
|
||||
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('AdHocPreviewStep').prop('hasErrors')).toBe(true);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('launch button should become active', async () => {
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
@ -184,7 +163,7 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
|
||||
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('td#check-action-item-1')
|
||||
@ -197,13 +176,17 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="Cred 1"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
expect(onLaunch).toHaveBeenCalledWith({
|
||||
become_enabled: '',
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useField } from 'formik';
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import { Form, FormGroup, Alert } from '@patternfly/react-core';
|
||||
import { CredentialsAPI } from 'api';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
@ -15,13 +15,17 @@ import ContentError from '../ContentError';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import OptionsList from '../OptionsList';
|
||||
|
||||
const CredentialErrorAlert = styled(Alert)`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const QS_CONFIG = getQSConfig('credentials', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function AdHocCredentialStep({ credentialTypeId, onEnableLaunch }) {
|
||||
function AdHocCredentialStep({ credentialTypeId }) {
|
||||
const history = useHistory();
|
||||
const {
|
||||
error,
|
||||
@ -72,10 +76,11 @@ function AdHocCredentialStep({ credentialTypeId, onEnableLaunch }) {
|
||||
fetchCredentials();
|
||||
}, [fetchCredentials]);
|
||||
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
const [field, meta, helpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(null),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
@ -83,68 +88,69 @@ function AdHocCredentialStep({ credentialTypeId, onEnableLaunch }) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
return (
|
||||
<Form>
|
||||
<FormGroup
|
||||
fieldId="credential"
|
||||
label={t`Machine Credential`}
|
||||
aria-label={t`Machine Credential`}
|
||||
isRequired
|
||||
validated={
|
||||
!credentialMeta.touched || !credentialMeta.error ? 'default' : 'error'
|
||||
}
|
||||
helperTextInvalid={credentialMeta.error}
|
||||
labelIcon={
|
||||
<Popover
|
||||
content={t`Select the credential you want to use when accessing the remote hosts to run the command. Choose the credential containing the username and SSH key or password that Ansible will need to log into the remote hosts.`}
|
||||
<>
|
||||
{meta.touched && meta.error && (
|
||||
<CredentialErrorAlert variant="danger" isInline title={meta.error} />
|
||||
)}
|
||||
<Form>
|
||||
<FormGroup
|
||||
fieldId="credential"
|
||||
label={t`Machine Credential`}
|
||||
aria-label={t`Machine Credential`}
|
||||
isRequired
|
||||
validated={!meta.touched || !meta.error ? 'default' : 'error'}
|
||||
helperTextInvalid={meta.error}
|
||||
labelIcon={
|
||||
<Popover
|
||||
content={t`Select the credential you want to use when accessing the remote hosts to run the command. Choose the credential containing the username and SSH key or password that Ansible will need to log into the remote hosts.`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<OptionsList
|
||||
value={field.value || []}
|
||||
options={credentials}
|
||||
optionCount={credentialCount}
|
||||
header={t`Machine Credential`}
|
||||
readOnly
|
||||
qsConfig={QS_CONFIG}
|
||||
searchColumns={[
|
||||
{
|
||||
name: t`Name`,
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: t`Created By (Username)`,
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: t`Modified By (Username)`,
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: t`Name`,
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
name="credential"
|
||||
selectItem={value => {
|
||||
helpers.setValue([value]);
|
||||
}}
|
||||
deselectItem={() => {
|
||||
helpers.setValue([]);
|
||||
}}
|
||||
searchableKeys={searchableKeys}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<OptionsList
|
||||
value={credentialField.value || []}
|
||||
options={credentials}
|
||||
optionCount={credentialCount}
|
||||
header={t`Machine Credential`}
|
||||
readOnly
|
||||
qsConfig={QS_CONFIG}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
searchableKeys={searchableKeys}
|
||||
searchColumns={[
|
||||
{
|
||||
name: t`Name`,
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: t`Created By (Username)`,
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: t`Modified By (Username)`,
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: t`Name`,
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
name="credential"
|
||||
selectItem={value => {
|
||||
credentialHelpers.setValue([value]);
|
||||
onEnableLaunch();
|
||||
}}
|
||||
deselectItem={() => {
|
||||
credentialHelpers.setValue([]);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AdHocCredentialStep.propTypes = {
|
||||
credentialTypeId: PropTypes.number.isRequired,
|
||||
onEnableLaunch: PropTypes.func.isRequired,
|
||||
};
|
||||
export default AdHocCredentialStep;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import React from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useField } from 'formik';
|
||||
@ -41,12 +40,14 @@ function AdHocDetailsStep({ verbosityOptions, moduleOptions }) {
|
||||
|
||||
const argumentsRequired =
|
||||
moduleNameField.value === 'command' || moduleNameField.value === 'shell';
|
||||
const [, argumentsMeta, argumentsHelpers] = useField({
|
||||
const [argumentsField, argumentsMeta, argumentsHelpers] = useField({
|
||||
name: 'module_args',
|
||||
validate: argumentsRequired && required(null),
|
||||
});
|
||||
|
||||
const isValid = !argumentsMeta.error || !argumentsMeta.touched;
|
||||
const isValid = argumentsRequired
|
||||
? (!argumentsMeta.error || !argumentsMeta.touched) && argumentsField.value
|
||||
: true;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -103,10 +104,7 @@ function AdHocDetailsStep({ verbosityOptions, moduleOptions }) {
|
||||
label={t`Arguments`}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
onBlur={() => argumentsHelpers.setTouched(true)}
|
||||
isRequired={
|
||||
moduleNameField.value === 'command' ||
|
||||
moduleNameField.value === 'shell'
|
||||
}
|
||||
isRequired={argumentsRequired}
|
||||
tooltip={
|
||||
moduleNameField.value ? (
|
||||
<>
|
||||
@ -249,7 +247,6 @@ function AdHocDetailsStep({ verbosityOptions, moduleOptions }) {
|
||||
</FormColumnLayout>
|
||||
|
||||
<VariablesField
|
||||
css="margin: 20px 0"
|
||||
id="extra_vars"
|
||||
name="extra_vars"
|
||||
value={JSON.stringify(variablesField.value)}
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Tooltip } from '@patternfly/react-core';
|
||||
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import { toTitleCase } from '../../util/strings';
|
||||
import { VariablesDetail } from '../CodeEditor';
|
||||
import { jsonToYaml } from '../../util/yaml';
|
||||
import { DetailList, Detail } from '../DetailList';
|
||||
|
||||
const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
||||
margin-left: 10px;
|
||||
margin-top: -2px;
|
||||
`;
|
||||
|
||||
const ErrorMessageWrapper = styled.div`
|
||||
align-items: center;
|
||||
color: var(--pf-global--danger-color--200);
|
||||
display: flex;
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
function AdHocPreviewStep({ hasErrors, values }) {
|
||||
const { credential, execution_environment, extra_vars } = values;
|
||||
|
||||
const items = Object.entries(values);
|
||||
return (
|
||||
<>
|
||||
{hasErrors && (
|
||||
<ErrorMessageWrapper>
|
||||
{t`Some of the previous step(s) have errors`}
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={t`See errors on the left`}
|
||||
trigger="click mouseenter focus"
|
||||
>
|
||||
<ExclamationCircleIcon />
|
||||
</Tooltip>
|
||||
</ErrorMessageWrapper>
|
||||
)}
|
||||
<DetailList gutter="sm">
|
||||
{items.map(
|
||||
([key, value]) =>
|
||||
key !== 'extra_vars' &&
|
||||
key !== 'execution_environment' &&
|
||||
key !== 'credential' && (
|
||||
<Detail key={key} label={toTitleCase(key)} value={value} />
|
||||
)
|
||||
)}
|
||||
{credential && (
|
||||
<Detail label={t`Credential`} value={credential[0]?.name} />
|
||||
)}
|
||||
{execution_environment && (
|
||||
<Detail
|
||||
label={t`Execution Environment`}
|
||||
value={execution_environment[0]?.name}
|
||||
/>
|
||||
)}
|
||||
{extra_vars && (
|
||||
<VariablesDetail
|
||||
value={jsonToYaml(JSON.stringify(extra_vars))}
|
||||
rows={4}
|
||||
label={t`Variables`}
|
||||
name="extra_vars"
|
||||
/>
|
||||
)}
|
||||
</DetailList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdHocPreviewStep;
|
||||
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useField } from 'formik';
|
||||
import { t } from '@lingui/macro';
|
||||
import StepName from '../LaunchPrompt/steps/StepName';
|
||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||
|
||||
const STEP_ID = 'credential';
|
||||
export default function useAdHocExecutionEnvironmentStep(
|
||||
visited,
|
||||
credentialTypeId
|
||||
) {
|
||||
const [field, meta, helpers] = useField('credential');
|
||||
const hasError =
|
||||
Object.keys(visited).includes('credential') &&
|
||||
!field.value.length &&
|
||||
meta.touched;
|
||||
|
||||
return {
|
||||
step: {
|
||||
id: STEP_ID,
|
||||
key: 3,
|
||||
name: (
|
||||
<StepName hasErrors={hasError} id="credential-step">
|
||||
{t`Credential`}
|
||||
</StepName>
|
||||
),
|
||||
component: <AdHocCredentialStep credentialTypeId={credentialTypeId} />,
|
||||
enableNext: true,
|
||||
nextButtonText: t`Next`,
|
||||
},
|
||||
hasError,
|
||||
validate: () => {
|
||||
if (!meta.value.length) {
|
||||
helpers.setError('A credential must be selected');
|
||||
}
|
||||
},
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('credential', true, false);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useFormikContext } from 'formik';
|
||||
import StepName from '../LaunchPrompt/steps/StepName';
|
||||
import AdHocDetailsStep from './AdHocDetailsStep';
|
||||
|
||||
const STEP_ID = 'details';
|
||||
export default function useAdHocDetailsStep(
|
||||
visited,
|
||||
moduleOptions,
|
||||
verbosityOptions
|
||||
) {
|
||||
const { values, touched, setFieldError } = useFormikContext();
|
||||
|
||||
const hasError = () => {
|
||||
if (!Object.keys(visited).includes(STEP_ID)) {
|
||||
return false;
|
||||
}
|
||||
if (!values.module_name && touched.module_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (values.module_name === 'shell' || values.module_name === 'command') {
|
||||
if (values.module_args) {
|
||||
return false;
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return {
|
||||
step: {
|
||||
id: STEP_ID,
|
||||
key: 1,
|
||||
name: (
|
||||
<StepName hasErrors={hasError()} id="details-step">
|
||||
{t`Details`}
|
||||
</StepName>
|
||||
),
|
||||
component: (
|
||||
<AdHocDetailsStep
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
/>
|
||||
),
|
||||
enableNext: true,
|
||||
nextButtonText: t`Next`,
|
||||
},
|
||||
hasError: hasError(),
|
||||
validate: () => {
|
||||
if (Object.keys(touched).includes('module_name' || 'module_args')) {
|
||||
if (!values.module_name) {
|
||||
setFieldError('module_name', t`This field is must not be blank.`);
|
||||
}
|
||||
if (
|
||||
values.module_name === ('command' || 'shell') &&
|
||||
!values.module_args
|
||||
) {
|
||||
setFieldError('module_args', t`This field is must not be blank`);
|
||||
}
|
||||
}
|
||||
},
|
||||
setTouched: setFieldTouched => {
|
||||
setFieldTouched('module_name', true, false);
|
||||
setFieldTouched('module_args', true, false);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import StepName from '../LaunchPrompt/steps/StepName';
|
||||
import AdHocExecutionEnvironmentStep from './AdHocExecutionEnvironmentStep';
|
||||
|
||||
const STEP_ID = 'executionEnvironment';
|
||||
export default function useAdHocExecutionEnvironmentStep(organizationId) {
|
||||
return {
|
||||
step: {
|
||||
id: STEP_ID,
|
||||
key: 2,
|
||||
stepNavItemProps: { style: { whiteSpace: 'nowrap' } },
|
||||
name: (
|
||||
<StepName hasErrors={false} id="executionEnvironment-step">
|
||||
{t`Execution Environment`}
|
||||
</StepName>
|
||||
),
|
||||
component: (
|
||||
<AdHocExecutionEnvironmentStep organizationId={organizationId} />
|
||||
),
|
||||
enableNext: true,
|
||||
nextButtonText: t`Next`,
|
||||
},
|
||||
hasError: false,
|
||||
validate: () => {},
|
||||
setTouched: () => {},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import { useState } from 'react';
|
||||
import useAdHocDetailsStep from './useAdHocDetailsStep';
|
||||
import useAdHocExecutionEnvironmentStep from './useAdHocExecutionEnvironmentStep';
|
||||
import useAdHocCredentialStep from './useAdHocCredentialStep';
|
||||
import useAdHocPreviewStep from './useAdHocPreviewStep';
|
||||
|
||||
export default function useAdHocLaunchSteps(
|
||||
moduleOptions,
|
||||
verbosityOptions,
|
||||
organizationId,
|
||||
credentialTypeId
|
||||
) {
|
||||
const [visited, setVisited] = useState({});
|
||||
const steps = [
|
||||
useAdHocDetailsStep(visited, moduleOptions, verbosityOptions),
|
||||
useAdHocExecutionEnvironmentStep(organizationId),
|
||||
useAdHocCredentialStep(visited, credentialTypeId),
|
||||
];
|
||||
|
||||
const hasErrors = steps.some(step => step.hasError);
|
||||
|
||||
steps.push(useAdHocPreviewStep(hasErrors));
|
||||
return {
|
||||
steps: steps.map(s => s.step).filter(s => s != null),
|
||||
validateStep: stepId =>
|
||||
steps
|
||||
.find(s => {
|
||||
return s?.step.id === stepId;
|
||||
})
|
||||
.validate(),
|
||||
visitStep: (prevStepId, setFieldTouched) => {
|
||||
setVisited({
|
||||
...visited,
|
||||
[prevStepId]: true,
|
||||
});
|
||||
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||
},
|
||||
visitAllSteps: setFieldTouched => {
|
||||
setVisited({
|
||||
details: true,
|
||||
executionEnvironment: true,
|
||||
credential: true,
|
||||
preview: true,
|
||||
});
|
||||
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useFormikContext } from 'formik';
|
||||
import StepName from '../LaunchPrompt/steps/StepName';
|
||||
import AdHocPreviewStep from './AdHocPreviewStep';
|
||||
|
||||
const STEP_ID = 'preview';
|
||||
export default function useAdHocPreviewStep(hasErrors) {
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return {
|
||||
step: {
|
||||
id: STEP_ID,
|
||||
key: 4,
|
||||
name: (
|
||||
<StepName hasErrors={false} id="preview-step">
|
||||
{t`Preview`}
|
||||
</StepName>
|
||||
),
|
||||
component: <AdHocPreviewStep hasErrors={hasErrors} values={values} />,
|
||||
enableNext: !hasErrors,
|
||||
nextButtonText: t`Launch`,
|
||||
},
|
||||
hasErrors: false,
|
||||
validate: () => {},
|
||||
setTouched: () => {},
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user