converting prompt steps to hook-based approach

This commit is contained in:
Keith Grant 2020-05-01 15:30:48 -07:00
parent 5c2eebf692
commit 11752e123d
16 changed files with 548 additions and 163 deletions

View File

@ -0,0 +1,223 @@
import React, { useState, useCallback, useEffect } from 'react';
import { Wizard } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik } from 'formik';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
import useRequest from '@util/useRequest';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import { required } from '@util/validators';
import InventoryStep from './InventoryStep';
import CredentialsStep from './CredentialsStep';
import OtherPromptsStep from './OtherPromptsStep';
import SurveyStep from './SurveyStep';
import PreviewStep from './PreviewStep';
import PromptFooter from './PromptFooter';
import mergeExtraVars from './mergeExtraVars';
const STEPS = {
INVENTORY: 'inventory',
CREDENTIALS: 'credentials',
PASSWORDS: 'passwords',
OTHER_PROMPTS: 'other',
SURVEY: 'survey',
PREVIEW: 'preview',
};
function showOtherPrompts(config) {
return (
config.ask_job_type_on_launch ||
config.ask_limit_on_launch ||
config.ask_verbosity_on_launch ||
config.ask_tags_on_launch ||
config.ask_skip_tags_on_launch ||
config.ask_variables_on_launch ||
config.ask_scm_branch_on_launch ||
config.ask_diff_mode_on_launch
);
}
function getInitialVisitedSteps(config) {
const visited = {};
if (config.ask_inventory_on_launch) {
visited[STEPS.INVENTORY] = false;
}
if (config.ask_credential_on_launch) {
visited[STEPS.CREDENTIALS] = false;
}
if (showOtherPrompts(config)) {
visited[STEPS.OTHER_PROMPTS] = false;
}
if (config.survey_enabled) {
visited[STEPS.SURVEY] = false;
}
return visited;
}
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
const [formErrors, setFormErrors] = useState({});
const [visitedSteps, setVisitedSteps] = useState(
getInitialVisitedSteps(config)
);
const {
result: survey,
request: fetchSurvey,
error: surveyError,
} = useRequest(
useCallback(async () => {
if (!config.survey_enabled) {
return {};
}
const { data } =
resource.type === 'workflow_job_template'
? await WorkflowJobTemplatesAPI.readSurvey(resource.id)
: await JobTemplatesAPI.readSurvey(resource.id);
return data;
}, [config.survey_enabled, resource])
);
useEffect(() => {
fetchSurvey();
}, [fetchSurvey]);
if (surveyError) {
return <ContentError error={surveyError} />;
}
if (config.survey_enabled && !survey) {
return <ContentLoading />;
}
const steps = [];
const initialValues = {};
if (config.ask_inventory_on_launch) {
initialValues.inventory = resource?.summary_fields?.inventory || null;
steps.push({
id: STEPS.INVENTORY,
name: i18n._(t`Inventory`),
component: <InventoryStep />,
});
}
if (config.ask_credential_on_launch) {
initialValues.credentials = resource?.summary_fields?.credentials || [];
steps.push({
id: STEPS.CREDENTIALS,
name: i18n._(t`Credentials`),
component: <CredentialsStep />,
});
}
// TODO: Add Credential Passwords step
if (config.ask_job_type_on_launch) {
initialValues.job_type = resource.job_type || '';
}
if (config.ask_limit_on_launch) {
initialValues.limit = resource.limit || '';
}
if (config.ask_verbosity_on_launch) {
initialValues.verbosity = resource.verbosity || 0;
}
if (config.ask_tags_on_launch) {
initialValues.job_tags = resource.job_tags || '';
}
if (config.ask_skip_tags_on_launch) {
initialValues.skip_tags = resource.skip_tags || '';
}
if (config.ask_variables_on_launch) {
initialValues.extra_vars = resource.extra_vars || '---';
}
if (config.ask_scm_branch_on_launch) {
initialValues.scm_branch = resource.scm_branch || '';
}
if (config.ask_diff_mode_on_launch) {
initialValues.diff_mode = resource.diff_mode || false;
}
if (showOtherPrompts(config)) {
steps.push({
id: STEPS.OTHER_PROMPTS,
name: i18n._(t`Other Prompts`),
component: <OtherPromptsStep config={config} />,
});
}
if (config.survey_enabled) {
initialValues.survey = {};
// survey.spec.forEach(question => {
// initialValues[`survey_${question.variable}`] = question.default;
// })
steps.push({
id: STEPS.SURVEY,
name: i18n._(t`Survey`),
component: <SurveyStep survey={survey} />,
});
}
steps.push({
id: STEPS.PREVIEW,
name: i18n._(t`Preview`),
component: (
<PreviewStep
resource={resource}
config={config}
survey={survey}
formErrors={formErrors}
/>
),
enableNext: Object.keys(formErrors).length === 0,
nextButtonText: i18n._(t`Launch`),
});
const validate = values => {
// return {};
return { limit: ['required field'] };
};
const submit = values => {
const postValues = {};
const setValue = (key, value) => {
if (typeof value !== 'undefined' && value !== null) {
postValues[key] = value;
}
};
setValue('inventory_id', values.inventory?.id);
setValue('credentials', values.credentials?.map(c => c.id));
setValue('job_type', values.job_type);
setValue('limit', values.limit);
setValue('job_tags', values.job_tags);
setValue('skip_tags', values.skip_tags);
setValue('extra_vars', mergeExtraVars(values.extra_vars, values.survey));
onLaunch(postValues);
};
console.log('formErrors:', formErrors);
return (
<Formik initialValues={initialValues} onSubmit={submit} validate={validate}>
{({ errors, values, touched, validateForm, handleSubmit }) => (
<Wizard
isOpen
onClose={onCancel}
onSave={handleSubmit}
onNext={async (nextStep, prevStep) => {
// console.log(`${prevStep.prevName} -> ${nextStep.name}`);
// console.log('errors', errors);
// console.log('values', values);
const newErrors = await validateForm();
setFormErrors(newErrors);
// console.log('new errors:', newErrors);
}}
onGoToStep={async (newStep, prevStep) => {
// console.log('errors', errors);
// console.log('values', values);
const newErrors = await validateForm();
setFormErrors(newErrors);
}}
title={i18n._(t`Prompts`)}
steps={steps}
// footer={<PromptFooter firstStep={steps[0].id} />}
/>
)}
</Formik>
);
}
export { LaunchPrompt as _LaunchPrompt };
export default withI18n()(LaunchPrompt);

View File

@ -3,174 +3,42 @@ import { Wizard } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik } from 'formik';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
import useRequest from '@util/useRequest';
// import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
// import useRequest from '@util/useRequest';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import { required } from '@util/validators';
import InventoryStep from './InventoryStep';
import CredentialsStep from './CredentialsStep';
import OtherPromptsStep from './OtherPromptsStep';
import SurveyStep from './SurveyStep';
import PreviewStep from './PreviewStep';
import PromptFooter from './PromptFooter';
// import { required } from '@util/validators';
// import InventoryStep from './InventoryStep';
// import CredentialsStep from './CredentialsStep';
// import OtherPromptsStep from './OtherPromptsStep';
// import SurveyStep from './SurveyStep';
// import PreviewStep from './PreviewStep';
// import PromptFooter from './PromptFooter';
import mergeExtraVars from './mergeExtraVars';
const STEPS = {
INVENTORY: 'inventory',
CREDENTIALS: 'credentials',
PASSWORDS: 'passwords',
OTHER_PROMPTS: 'other',
SURVEY: 'survey',
PREVIEW: 'preview',
};
function showOtherPrompts(config) {
return (
config.ask_job_type_on_launch ||
config.ask_limit_on_launch ||
config.ask_verbosity_on_launch ||
config.ask_tags_on_launch ||
config.ask_skip_tags_on_launch ||
config.ask_variables_on_launch ||
config.ask_scm_branch_on_launch ||
config.ask_diff_mode_on_launch
);
}
function getInitialVisitedSteps(config) {
const visited = {};
if (config.ask_inventory_on_launch) {
visited[STEPS.INVENTORY] = false;
}
if (config.ask_credential_on_launch) {
visited[STEPS.CREDENTIALS] = false;
}
if (showOtherPrompts(config)) {
visited[STEPS.OTHER_PROMPTS] = false;
}
if (config.survey_enabled) {
visited[STEPS.SURVEY] = false;
}
return visited;
}
import { useSteps, useVisitedSteps } from './hooks';
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
const [formErrors, setFormErrors] = useState({});
const [visitedSteps, setVisitedSteps] = useState(
getInitialVisitedSteps(config)
const { steps, initialValues, isReady, contentError } = useSteps(
config,
resource,
i18n
);
const [visitedSteps, visitStep] = useVisitedSteps(config);
const {
result: survey,
request: fetchSurvey,
error: surveyError,
} = useRequest(
useCallback(async () => {
if (!config.survey_enabled) {
return {};
}
const { data } =
resource.type === 'workflow_job_template'
? await WorkflowJobTemplatesAPI.readSurvey(resource.id)
: await JobTemplatesAPI.readSurvey(resource.id);
return data;
}, [config.survey_enabled, resource])
);
useEffect(() => {
fetchSurvey();
}, [fetchSurvey]);
if (surveyError) {
return <ContentError error={surveyError} />;
if (contentError) {
return <ContentError error={contentError} />;
}
if (config.survey_enabled && !survey) {
if (!isReady) {
return <ContentLoading />;
}
const steps = [];
const initialValues = {};
if (config.ask_inventory_on_launch) {
initialValues.inventory = resource?.summary_fields?.inventory || null;
steps.push({
id: STEPS.INVENTORY,
name: i18n._(t`Inventory`),
component: <InventoryStep />,
});
}
if (config.ask_credential_on_launch) {
initialValues.credentials = resource?.summary_fields?.credentials || [];
steps.push({
id: STEPS.CREDENTIALS,
name: i18n._(t`Credentials`),
component: <CredentialsStep />,
});
}
// TODO: Add Credential Passwords step
if (config.ask_job_type_on_launch) {
initialValues.job_type = resource.job_type || '';
}
if (config.ask_limit_on_launch) {
initialValues.limit = resource.limit || '';
}
if (config.ask_verbosity_on_launch) {
initialValues.verbosity = resource.verbosity || 0;
}
if (config.ask_tags_on_launch) {
initialValues.job_tags = resource.job_tags || '';
}
if (config.ask_skip_tags_on_launch) {
initialValues.skip_tags = resource.skip_tags || '';
}
if (config.ask_variables_on_launch) {
initialValues.extra_vars = resource.extra_vars || '---';
}
if (config.ask_scm_branch_on_launch) {
initialValues.scm_branch = resource.scm_branch || '';
}
if (config.ask_diff_mode_on_launch) {
initialValues.diff_mode = resource.diff_mode || false;
}
if (showOtherPrompts(config)) {
steps.push({
id: STEPS.OTHER_PROMPTS,
name: i18n._(t`Other Prompts`),
component: <OtherPromptsStep config={config} />,
});
}
if (config.survey_enabled) {
initialValues.survey = {};
// survey.spec.forEach(question => {
// initialValues[`survey_${question.variable}`] = question.default;
// })
steps.push({
id: STEPS.SURVEY,
name: i18n._(t`Survey`),
component: <SurveyStep survey={survey} />,
});
}
steps.push({
id: STEPS.PREVIEW,
name: i18n._(t`Preview`),
component: (
<PreviewStep
resource={resource}
config={config}
survey={survey}
formErrors={formErrors}
/>
),
enableNext: Object.keys(formErrors).length === 0,
nextButtonText: i18n._(t`Launch`),
});
// TODO move into hook?
const validate = values => {
// return {};
return { limit: ['required field'] };
};
// TODO move into hook?
const submit = values => {
const postValues = {};
const setValue = (key, value) => {
@ -187,7 +55,6 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
setValue('extra_vars', mergeExtraVars(values.extra_vars, values.survey));
onLaunch(postValues);
};
console.log('formErrors:', formErrors);
return (
<Formik initialValues={initialValues} onSubmit={submit} validate={validate}>
@ -197,22 +64,19 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
onClose={onCancel}
onSave={handleSubmit}
onNext={async (nextStep, prevStep) => {
// console.log(`${prevStep.prevName} -> ${nextStep.name}`);
// console.log('errors', errors);
// console.log('values', values);
console.log(prevStep);
visitStep(prevStep.id);
const newErrors = await validateForm();
setFormErrors(newErrors);
// console.log('new errors:', newErrors);
// updatePromptErrors(prevStep.prevName, newErrors);
}}
onGoToStep={async (newStep, prevStep) => {
// console.log('errors', errors);
// console.log('values', values);
console.log(prevStep);
visitStep(prevStep.id);
const newErrors = await validateForm();
setFormErrors(newErrors);
// updatePromptErrors(prevStep.prevName, newErrors);
}}
title={i18n._(t`Prompts`)}
steps={steps}
// footer={<PromptFooter firstStep={steps[0].id} />}
/>
)}
</Formik>

View File

@ -0,0 +1,70 @@
import { useState } from 'react';
import useInventoryStep from './steps/useInventoryStep';
import useCredentialsStep from './steps/useCredentialsStep';
import useOtherPromptsStep from './steps/useOtherPromptsStep';
import useSurveyStep from './steps/useSurveyStep';
import usePreviewStep from './steps/usePreviewStep';
// const INVENTORY = 'inventory';
// const CREDENTIALS = 'credentials';
// const PASSWORDS = 'passwords';
// const OTHER_PROMPTS = 'other';
// const SURVEY = 'survey';
// const PREVIEW = 'preview';
export function useSteps(config, resource, i18n) {
// TODO pass in form errors?
const formErrors = {};
const inventory = useInventoryStep(config, resource, i18n);
const credentials = useCredentialsStep(config, resource, i18n);
const otherPrompts = useOtherPromptsStep(config, resource, i18n);
const survey = useSurveyStep(config, resource, i18n);
const preview = usePreviewStep(
config,
resource,
survey.survey,
formErrors,
i18n
);
// TODO useState for steps to track dynamic steps (credentialPasswords)?
const steps = [
inventory.step,
credentials.step,
otherPrompts.step,
survey.step,
preview.step,
].filter(step => step !== null);
const initialValues = {
...inventory.initialValues,
...credentials.initialValues,
...otherPrompts.initialValues,
...survey.initialValues,
};
const isReady =
inventory.isReady &&
credentials.isReady &&
otherPrompts.isReady &&
survey.isReady &&
preview.isReady;
const contentError =
inventory.error ||
credentials.error ||
otherPrompts.error ||
survey.error ||
preview.error;
return { steps, initialValues, isReady, contentError };
}
export function usePromptErrors(config) {
const [promptErrors, setPromptErrors] = useState({});
const updatePromptErrors = () => {};
return [promptErrors, updatePromptErrors];
}
// TODO this interrelates with usePromptErrors
// merge? or pass result from one into the other?
export function useVisitedSteps(config) {
return [[], () => {}];
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { useFormikContext } from 'formik';
import yaml from 'js-yaml';
import PromptDetail from '@components/PromptDetail';
import mergeExtraVars, { maskPasswords } from './mergeExtraVars';
import mergeExtraVars, { maskPasswords } from '../mergeExtraVars';
function PreviewStep({ resource, config, survey, formErrors }) {
const { values } = useFormikContext();

View File

@ -0,0 +1,34 @@
import React from 'react';
import { t } from '@lingui/macro';
import CredentialsStep from './CredentialsStep';
const STEP_ID = 'credentials';
export default function useCredentialsStep(config, resource, i18n) {
return {
step: getStep(config, i18n),
initialValues: getInitialValues(config, resource),
isReady: true,
error: null,
};
}
function getStep(config, i18n) {
if (!config.ask_credential_on_launch) {
return null;
}
return {
id: STEP_ID,
name: i18n._(t`Credentials`),
component: <CredentialsStep i18n={i18n} />,
};
}
function getInitialValues(config, resource) {
if (!config.ask_credential_on_launch) {
return {};
}
return {
credentials: resource?.summary_fields?.credentials || [],
};
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { t } from '@lingui/macro';
import InventoryStep from './InventoryStep';
const STEP_ID = 'inventory';
export default function useInventoryStep(config, resource, i18n) {
return {
step: getStep(config, i18n),
initialValues: getInitialValues(config, resource),
isReady: true,
error: null,
};
}
function getStep(config, i18n) {
if (!config.ask_inventory_on_launch) {
return null;
}
return {
id: STEP_ID,
name: i18n._(t`Inventory`),
component: <InventoryStep i18n={i18n} />,
};
}
function getInitialValues(config, resource) {
if (!config.ask_inventory_on_launch) {
return {};
}
return {
inventory: resource?.summary_fields?.inventory || null,
};
}

View File

@ -0,0 +1,67 @@
import React from 'react';
import { t } from '@lingui/macro';
import OtherPromptsStep from './OtherPromptsStep';
const STEP_ID = 'other';
export default function useOtherPrompt(config, resource, i18n) {
return {
step: getStep(config, i18n),
initialValues: getInitialValues(config, resource),
isReady: true,
error: null,
};
}
function getStep(config, i18n) {
if (!shouldShowPrompt(config)) {
return null;
}
return {
id: STEP_ID,
name: i18n._(t`Other Prompts`),
component: <OtherPromptsStep config={config} i18n={i18n} />,
};
}
function shouldShowPrompt(config) {
return (
config.ask_job_type_on_launch ||
config.ask_limit_on_launch ||
config.ask_verbosity_on_launch ||
config.ask_tags_on_launch ||
config.ask_skip_tags_on_launch ||
config.ask_variables_on_launch ||
config.ask_scm_branch_on_launch ||
config.ask_diff_mode_on_launch
);
}
function getInitialValues(config, resource) {
const initialValues = {};
if (config.ask_job_type_on_launch) {
initialValues.job_type = resource.job_type || '';
}
if (config.ask_limit_on_launch) {
initialValues.limit = resource.limit || '';
}
if (config.ask_verbosity_on_launch) {
initialValues.verbosity = resource.verbosity || 0;
}
if (config.ask_tags_on_launch) {
initialValues.job_tags = resource.job_tags || '';
}
if (config.ask_skip_tags_on_launch) {
initialValues.skip_tags = resource.skip_tags || '';
}
if (config.ask_variables_on_launch) {
initialValues.extra_vars = resource.extra_vars || '---';
}
if (config.ask_scm_branch_on_launch) {
initialValues.scm_branch = resource.scm_branch || '';
}
if (config.ask_diff_mode_on_launch) {
initialValues.diff_mode = resource.diff_mode || false;
}
return initialValues;
}

View File

@ -0,0 +1,33 @@
import React from 'react';
import { t } from '@lingui/macro';
import PreviewStep from './PreviewStep';
const STEP_ID = 'preview';
export default function usePreviewStep(
config,
resource,
survey,
formErrors,
i18n
) {
return {
step: {
id: STEP_ID,
name: i18n._(t`Preview`),
component: (
<PreviewStep
config={config}
resource={resource}
survey={survey}
formErrors={formErrors}
/>
),
enableNext: Object.keys(formErrors).length === 0,
nextButtonText: i18n._(t`Launch`),
},
initialValues: {},
isReady: true,
error: null,
};
}

View File

@ -0,0 +1,60 @@
import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro';
import useRequest from '@util/useRequest';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
import SurveyStep from './SurveyStep';
const STEP_ID = 'survey';
export default function useSurveyStep(config, resource, i18n) {
const { result: survey, request: fetchSurvey, isLoading, error } = useRequest(
useCallback(async () => {
if (!config.survey_enabled) {
return {};
}
const { data } =
resource.type === 'workflow_job_template'
? await WorkflowJobTemplatesAPI.readSurvey(resource.id)
: await JobTemplatesAPI.readSurvey(resource.id);
return data;
}, [config.survey_enabled, resource])
);
useEffect(() => {
fetchSurvey();
}, [fetchSurvey]);
return {
step: getStep(config, survey, i18n),
initialValues: getInitialValues(config, survey),
survey,
isReady: !isLoading && !!survey,
error,
};
}
function getStep(config, survey, i18n) {
if (!config.survey_enabled) {
return null;
}
return {
id: STEP_ID,
name: i18n._(t`Survey`),
component: <SurveyStep survey={survey} i18n={i18n} />,
};
}
function getInitialValues(config, survey) {
if (!config.survey_enabled || !survey) {
return {};
}
const values = {};
survey.spec.forEach(question => {
if (question.type === 'multiselect') {
values[`survey_${question.variable}`] = question.default.split('\n');
} else {
values[`survey_${question.variable}`] = question.default;
}
});
return values;
}