diff --git a/awx/ui_next/src/api/models/SystemJobTemplates.js b/awx/ui_next/src/api/models/SystemJobTemplates.js index 5e8b395821..f9bd96d661 100644 --- a/awx/ui_next/src/api/models/SystemJobTemplates.js +++ b/awx/ui_next/src/api/models/SystemJobTemplates.js @@ -10,12 +10,6 @@ class SystemJobTemplates extends Mixins { this.baseUrl = '/api/v2/system_job_templates/'; } - readDetail(id) { - const path = `${this.baseUrl}${id}/`; - - return this.http.get(path).then(({ data }) => data); - } - launch(id, data) { return this.http.post(`${this.baseUrl}${id}/launch/`, data); } diff --git a/awx/ui_next/src/components/PromptDetail/PromptDetail.js b/awx/ui_next/src/components/PromptDetail/PromptDetail.js index b23eed0bd9..c58a1f99d6 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptDetail.js +++ b/awx/ui_next/src/components/PromptDetail/PromptDetail.js @@ -1,18 +1,15 @@ import 'styled-components/macro'; import React from 'react'; import { shape } from 'prop-types'; - import { t, Trans } from '@lingui/macro'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { Chip, Divider, Title } from '@patternfly/react-core'; import { toTitleCase } from 'util/strings'; - import CredentialChip from '../CredentialChip'; import ChipGroup from '../ChipGroup'; import { DetailList, Detail, UserDateDetail } from '../DetailList'; import { VariablesDetail } from '../CodeEditor'; - import PromptProjectDetail from './PromptProjectDetail'; import PromptInventorySourceDetail from './PromptInventorySourceDetail'; import PromptJobTemplateDetail from './PromptJobTemplateDetail'; @@ -139,123 +136,136 @@ function PromptDetail({ resource, launchConfig = {}, overrides = {} }) { user={details?.summary_fields?.modified_by} /> )} + {details?.type === 'system_job_template' && ( + + )} - {hasPromptData(launchConfig) && hasOverrides && ( - <> - {t`Prompted Values`} - - - {launchConfig.ask_job_type_on_launch && ( - - )} - {launchConfig.ask_credential_on_launch && ( - - {overrides.credentials.map((cred) => ( - - ))} - - } - /> - )} - {launchConfig.ask_inventory_on_launch && ( - - )} - {launchConfig.ask_scm_branch_on_launch && ( - - )} - {launchConfig.ask_limit_on_launch && ( - - )} - {Object.prototype.hasOwnProperty.call(overrides, 'verbosity') && - launchConfig.ask_verbosity_on_launch ? ( - - ) : null} - {launchConfig.ask_tags_on_launch && ( - - {overrides.job_tags.length > 0 && - overrides.job_tags.split(',').map((jobTag) => ( - - {jobTag} - + {details?.type !== 'system_job_template' && + hasPromptData(launchConfig) && + hasOverrides && ( + <> + {t`Prompted Values`} + + + {launchConfig.ask_job_type_on_launch && ( + + )} + {launchConfig.ask_credential_on_launch && ( + + {overrides.credentials.map((cred) => ( + ))} - - } - /> - )} - {launchConfig.ask_skip_tags_on_launch && ( - - {overrides.skip_tags.length > 0 && - overrides.skip_tags.split(',').map((skipTag) => ( - - {skipTag} - - ))} - - } - /> - )} - {launchConfig.ask_diff_mode_on_launch && ( - - )} - {(launchConfig.survey_enabled || - launchConfig.ask_variables_on_launch) && ( - - )} - - - )} + + } + /> + )} + {launchConfig.ask_inventory_on_launch && ( + + )} + {launchConfig.ask_scm_branch_on_launch && ( + + )} + {launchConfig.ask_limit_on_launch && ( + + )} + {Object.prototype.hasOwnProperty.call(overrides, 'verbosity') && + launchConfig.ask_verbosity_on_launch ? ( + + ) : null} + {launchConfig.ask_tags_on_launch && ( + + {overrides.job_tags.length > 0 && + overrides.job_tags.split(',').map((jobTag) => ( + + {jobTag} + + ))} + + } + /> + )} + {launchConfig.ask_skip_tags_on_launch && ( + + {overrides.skip_tags.length > 0 && + overrides.skip_tags.split(',').map((skipTag) => ( + + {skipTag} + + ))} + + } + /> + )} + {launchConfig.ask_diff_mode_on_launch && ( + + )} + {(launchConfig.survey_enabled || + launchConfig.ask_variables_on_launch) && ( + + )} + + + )} ); } diff --git a/awx/ui_next/src/components/Workflow/WorkflowLegend.js b/awx/ui_next/src/components/Workflow/WorkflowLegend.js index 96b008216c..eca1b82bef 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowLegend.js +++ b/awx/ui_next/src/components/Workflow/WorkflowLegend.js @@ -103,6 +103,10 @@ function WorkflowLegend() { P {t`Project Sync`} +
  • + M + {t`Management Job`} +
  • diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.js b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.js index 058a9531d8..0d6c61a01c 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.js +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.js @@ -62,6 +62,10 @@ function WorkflowNodeHelp({ node }) { case 'workflow_approval': nodeType = t`Workflow Approval`; break; + case 'system_job_template': + case 'system_job': + nodeType = t`Management Job`; + break; default: nodeType = ''; } diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.js b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.js index e0eb145528..b6a880e749 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.js +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.js @@ -53,6 +53,10 @@ function WorkflowNodeTypeLetter({ node }) { case 'inventory_update': nodeTypeLetter = 'I'; break; + case 'system_job_template': + case 'system_job': + nodeTypeLetter = 'M'; + break; case 'workflow_job_template': case 'workflow_job': nodeTypeLetter = 'W'; diff --git a/awx/ui_next/src/screens/ManagementJob/ManagementJob.js b/awx/ui_next/src/screens/ManagementJob/ManagementJob.js index e995b1fdce..6996554488 100644 --- a/awx/ui_next/src/screens/ManagementJob/ManagementJob.js +++ b/awx/ui_next/src/screens/ManagementJob/ManagementJob.js @@ -41,7 +41,7 @@ function ManagementJob({ setBreadcrumb }) { page_size: 1, role_level: 'notification_admin_role', }), - ]).then(([systemJobTemplate, notificationRoles]) => ({ + ]).then(([{ data: systemJobTemplate }, notificationRoles]) => ({ systemJobTemplate, notificationRoles, })), diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/DaysToKeepStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/DaysToKeepStep.jsx new file mode 100644 index 0000000000..bfb8266eb9 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/DaysToKeepStep.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { t } from '@lingui/macro'; +import { Form } from '@patternfly/react-core'; +import { useField } from 'formik'; +import { + required, + minMaxValue, + integer, + combine, +} from '../../../../../util/validators'; +import FormField from '../../../../../components/FormField'; + +function DaysToKeepStep() { + const [, meta] = useField('daysToKeep'); + const validators = [required(null), minMaxValue(0), integer()]; + return ( +
    { + e.preventDefault(); + }} + > + + + ); +} + +export default DaysToKeepStep; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/DaysToKeepStep.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/DaysToKeepStep.test.jsx new file mode 100644 index 0000000000..9c17f80cf7 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/DaysToKeepStep.test.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers'; +import DaysToKeepStep from './DaysToKeepStep'; + +let wrapper; + +describe('DaysToKeepStep', () => { + beforeAll(() => { + wrapper = mountWithContexts( + + + + ); + }); + + afterAll(() => { + wrapper.unmount(); + }); + + test('Days to keep field rendered correctly', () => { + expect(wrapper.find('FormField#days-to-keep').length).toBe(1); + expect(wrapper.find('FormField#days-to-keep').prop('isRequired')).toBe( + true + ); + expect(wrapper.find('input#days-to-keep').prop('value')).toBe(30); + }); +}); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeAddModal.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeAddModal.js index da34b4fd52..ac37f0e562 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeAddModal.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeAddModal.js @@ -56,7 +56,13 @@ function NodeAddModal() { ) { node.promptValues = values; } + if (values?.nodeType === 'system_job_template') { + node.promptValues = { + extra_data: values?.extra_data, + }; + } } + dispatch({ type: 'CREATE_NODE', node, diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.js index b83d99b4fc..762f07aeed 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.js @@ -44,6 +44,11 @@ function NodeEditModal() { node.launchConfig = config; } + if (nodeType === 'system_job_template') { + node.promptValues = { + extra_data: values?.extra_data, + }; + } } dispatch({ diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.js index 6789085a45..9b9f3fe785 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.js @@ -1,7 +1,6 @@ import 'styled-components/macro'; import React, { useContext, useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; - import { t } from '@lingui/macro'; import { Formik, useFormikContext } from 'formik'; import yaml from 'js-yaml'; @@ -27,12 +26,10 @@ import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from 'api'; import Wizard from 'components/Wizard'; import AlertModal from 'components/AlertModal'; import useWorkflowNodeSteps from './useWorkflowNodeSteps'; - import NodeNextButton from './NodeNextButton'; function NodeModalForm({ askLinkType, - onSave, title, credentialError, @@ -66,7 +63,6 @@ function NodeModalForm({ } = useWorkflowNodeSteps( launchConfig, surveyConfig, - values.nodeResource, askLinkType, resourceDefaultCredentials @@ -98,7 +94,18 @@ function NodeModalForm({ } values.extra_data = extraVars && parseVariableField(extraVars); delete values.extra_vars; + } else if ( + values.nodeType === 'system_job_template' && + ['cleanup_activitystream', 'cleanup_jobs'].includes( + values?.nodeResource?.job_type + ) + ) { + values.extra_data = { + days: parseInt(values?.daysToKeep, 10), + }; } + + delete values.daysToKeep; onSave(values, launchConfig); }; @@ -351,6 +358,7 @@ const NodeModal = ({ onSave, askLinkType, title }) => { initialValues={{ approvalName: '', approvalDescription: '', + daysToKeep: 30, timeoutMinutes: 0, timeoutSeconds: 0, convergence: 'any', diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.js index b5efb53901..9df1047ffd 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.js @@ -1,6 +1,5 @@ import 'styled-components/macro'; import React, { useState } from 'react'; - import { t, Trans } from '@lingui/macro'; import styled from 'styled-components'; import { useField } from 'formik'; @@ -24,6 +23,7 @@ import { useConfig } from 'contexts/Config'; import InventorySourcesList from './InventorySourcesList'; import JobTemplatesList from './JobTemplatesList'; import ProjectsList from './ProjectsList'; +import SystemJobTemplatesList from './SystemJobTemplatesList'; import WorkflowJobTemplatesList from './WorkflowJobTemplatesList'; const NodeTypeErrorAlert = styled(Alert)` @@ -99,6 +99,12 @@ function NodeTypeStep() { label: t`Project Sync`, isDisabled: false, }, + { + key: 'system_job_template', + value: 'system_job_template', + label: t`Management Job`, + isDisabled: false, + }, { key: 'workflow_job_template', value: 'workflow_job_template', @@ -137,6 +143,12 @@ function NodeTypeStep() { onUpdateNodeResource={nodeResourceHelpers.setValue} /> )} + {nodeTypeField.value === 'system_job_template' && ( + + )} {nodeTypeField.value === 'workflow_job_template' && ( { + const params = parseQueryString(QS_CONFIG, location.search); + const [response, actionsResponse] = await Promise.all([ + SystemJobTemplatesAPI.read(params, { + role_level: 'execute_role', + }), + SystemJobTemplatesAPI.readOptions(), + ]); + return { + systemJobTemplates: response.data.results, + count: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map((val) => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter((key) => actionsResponse.data.actions?.GET[key].filterable), + }; + }, [location]), + { + systemJobTemplates: [], + count: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + + useEffect(() => { + fetchWorkflowJobTemplates(); + }, [fetchWorkflowJobTemplates]); + + return ( + + {t`Name`} + + } + renderRow={(item, index) => ( + onUpdateNodeResource(item)} + onDeselect={() => onUpdateNodeResource(null)} + isRadio + /> + )} + renderToolbar={(props) => } + showPageSizeOptions={false} + toolbarSearchColumns={[ + { + name: t`Name`, + key: 'name__icontains', + isDefault: true, + }, + ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} + /> + ); +} + +SystemJobTemplatesList.propTypes = { + nodeResource: shape(), + onUpdateNodeResource: func.isRequired, +}; + +SystemJobTemplatesList.defaultProps = { + nodeResource: null, +}; + +export default SystemJobTemplatesList; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/SystemJobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/SystemJobTemplatesList.test.jsx new file mode 100644 index 0000000000..a4e8435001 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/SystemJobTemplatesList.test.jsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../../../../testUtils/enzymeHelpers'; +import { SystemJobTemplatesAPI } from '../../../../../../api'; +import SystemJobTemplatesList from './SystemJobTemplatesList'; + +jest.mock('../../../../../../api/models/SystemJobTemplates'); + +const nodeResource = { + id: 1, + type: 'system_job_template', + url: '/api/v2/system_job_templates/1/', + related: { + next_schedule: '/api/v2/schedules/1/', + jobs: '/api/v2/system_job_templates/1/jobs/', + schedules: '/api/v2/system_job_templates/1/schedules/', + launch: '/api/v2/system_job_templates/1/launch/', + notification_templates_started: + '/api/v2/system_job_templates/1/notification_templates_started/', + notification_templates_success: + '/api/v2/system_job_templates/1/notification_templates_success/', + notification_templates_error: + '/api/v2/system_job_templates/1/notification_templates_error/', + }, + summary_fields: {}, + created: '2021-06-29T18:58:47.571901Z', + modified: '2021-06-29T18:58:47.571901Z', + name: 'Cleanup Job Details', + description: 'Remove job history', + last_job_run: null, + last_job_failed: false, + next_job_run: '2021-07-04T18:58:47Z', + status: 'ok', + execution_environment: null, + job_type: 'cleanup_jobs', +}; +const onUpdateNodeResource = jest.fn(); + +describe('SystemJobTemplatesList', () => { + let wrapper; + afterEach(() => { + wrapper.unmount(); + }); + test('Row selected when nodeResource id matches row id and clicking new row makes expected callback', async () => { + SystemJobTemplatesAPI.read.mockResolvedValueOnce({ + data: { + count: 2, + results: [ + nodeResource, + { + id: 2, + type: 'system_job_template', + url: '/api/v2/system_job_templates/2/', + related: { + last_job: '/api/v2/system_jobs/3/', + next_schedule: '/api/v2/schedules/2/', + jobs: '/api/v2/system_job_templates/2/jobs/', + schedules: '/api/v2/system_job_templates/2/schedules/', + launch: '/api/v2/system_job_templates/2/launch/', + notification_templates_started: + '/api/v2/system_job_templates/2/notification_templates_started/', + notification_templates_success: + '/api/v2/system_job_templates/2/notification_templates_success/', + notification_templates_error: + '/api/v2/system_job_templates/2/notification_templates_error/', + }, + summary_fields: { + last_job: { + id: 3, + name: 'Cleanup Activity Stream', + description: 'Remove activity stream history', + finished: '2021-06-29T20:38:22.770364Z', + status: 'successful', + failed: false, + }, + last_update: { + id: 3, + name: 'Cleanup Activity Stream', + description: 'Remove activity stream history', + status: 'successful', + failed: false, + }, + }, + created: '2021-06-29T18:58:47.571901Z', + modified: '2021-06-29T18:58:47.571901Z', + name: 'Cleanup Activity Stream', + description: 'Remove activity stream history', + last_job_run: '2021-06-29T20:38:22.770364Z', + last_job_failed: false, + next_job_run: '2021-07-06T18:58:47Z', + status: 'successful', + execution_environment: null, + job_type: 'cleanup_activitystream', + }, + ], + }, + }); + SystemJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect( + wrapper.find('CheckboxListItem[name="Cleanup Job Details"]').props() + .isSelected + ).toBe(true); + expect( + wrapper.find('CheckboxListItem[name="Cleanup Activity Stream"]').props() + .isSelected + ).toBe(false); + wrapper + .find('CheckboxListItem[name="Cleanup Activity Stream"]') + .prop('onSelect')(); + expect(onUpdateNodeResource).toHaveBeenCalledWith({ + id: 2, + type: 'system_job_template', + url: '/api/v2/system_job_templates/2/', + related: { + last_job: '/api/v2/system_jobs/3/', + next_schedule: '/api/v2/schedules/2/', + jobs: '/api/v2/system_job_templates/2/jobs/', + schedules: '/api/v2/system_job_templates/2/schedules/', + launch: '/api/v2/system_job_templates/2/launch/', + notification_templates_started: + '/api/v2/system_job_templates/2/notification_templates_started/', + notification_templates_success: + '/api/v2/system_job_templates/2/notification_templates_success/', + notification_templates_error: + '/api/v2/system_job_templates/2/notification_templates_error/', + }, + summary_fields: { + last_job: { + id: 3, + name: 'Cleanup Activity Stream', + description: 'Remove activity stream history', + finished: '2021-06-29T20:38:22.770364Z', + status: 'successful', + failed: false, + }, + last_update: { + id: 3, + name: 'Cleanup Activity Stream', + description: 'Remove activity stream history', + status: 'successful', + failed: false, + }, + }, + created: '2021-06-29T18:58:47.571901Z', + modified: '2021-06-29T18:58:47.571901Z', + name: 'Cleanup Activity Stream', + description: 'Remove activity stream history', + last_job_run: '2021-06-29T20:38:22.770364Z', + last_job_failed: false, + next_job_run: '2021-07-06T18:58:47Z', + status: 'successful', + execution_environment: null, + job_type: 'cleanup_activitystream', + }); + }); + test('Error shown when read() request errors', async () => { + SystemJobTemplatesAPI.read.mockRejectedValue(new Error()); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js index 550319df41..4e7c01127e 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js @@ -136,7 +136,12 @@ function NodeViewModal({ readOnly }) { if (promptValues) { overrides = promptValues; - if (launchConfig.ask_variables_on_launch || launchConfig.survey_enabled) { + if ( + launchConfig.ask_variables_on_launch || + launchConfig.survey_enabled || + (fullUnifiedJobTemplate.type === 'system_job_template' && + promptValues.extra_data) + ) { overrides.extra_vars = jsonToYaml( JSON.stringify(promptValues.extra_data) ); @@ -150,7 +155,11 @@ function NodeViewModal({ readOnly }) { if (launchConfig.ask_scm_branch_on_launch) { overrides.scm_branch = originalNodeObject.scm_branch; } - if (launchConfig.ask_variables_on_launch || launchConfig.survey_enabled) { + if ( + launchConfig.ask_variables_on_launch || + launchConfig.survey_enabled || + fullUnifiedJobTemplate.type === 'system_job_template' + ) { overrides.extra_vars = jsonToYaml( JSON.stringify(originalNodeObject.extra_data) ); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useDaysToKeepStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useDaysToKeepStep.jsx new file mode 100644 index 0000000000..8990d8c8e7 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useDaysToKeepStep.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import DaysToKeepStep from './DaysToKeepStep'; +import StepName from '../../../../../components/LaunchPrompt/steps/StepName'; + +const STEP_ID = 'daysToKeep'; + +export default function useDaysToKeepStep() { + const [, nodeResourceMeta] = useField('nodeResource'); + const [, daysToKeepMeta] = useField('daysToKeep'); + + return { + step: getStep(nodeResourceMeta, daysToKeepMeta), + initialValues: { daysToKeep: 30 }, + isReady: true, + contentError: null, + hasError: !!daysToKeepMeta.error, + setTouched: (setFieldTouched) => { + setFieldTouched('daysToKeep', true, false); + }, + validate: () => {}, + }; +} +function getStep(nodeResourceMeta, daysToKeepMeta) { + if ( + ['cleanup_activitystream', 'cleanup_jobs'].includes( + nodeResourceMeta?.value?.job_type + ) + ) { + return { + id: STEP_ID, + name: ( + + {t`Days to keep`} + + ), + component: , + enableNext: !daysToKeepMeta.error, + }; + } + return null; +} diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js index b5cd974d16..c91313952d 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js @@ -9,6 +9,7 @@ import usePreviewStep from 'components/LaunchPrompt/steps/usePreviewStep'; import { WorkflowStateContext } from 'contexts/Workflow'; import { jsonToYaml } from 'util/yaml'; import useNodeTypeStep from './NodeTypeStep/useNodeTypeStep'; +import useDaysToKeepStep from './useDaysToKeepStep'; import useRunTypeStep from './useRunTypeStep'; function showPreviewStep(nodeType, launchConfig) { @@ -59,6 +60,34 @@ const getNodeToEditDefaultValues = ( return initialValues; } + if (nodeToEdit?.fullUnifiedJobTemplate?.type === 'system_job_template') { + if ( + nodeToEdit?.promptValues?.extra_data && + Object.prototype.hasOwnProperty.call( + nodeToEdit.promptValues.extra_data, + 'days' + ) + ) { + initialValues.daysToKeep = parseInt( + nodeToEdit.promptValues.extra_data.days, + 10 + ); + } else if ( + nodeToEdit?.originalNodeObject?.extra_data && + Object.prototype.hasOwnProperty.call( + nodeToEdit.originalNodeObject.extra_data, + 'days' + ) + ) { + initialValues.daysToKeep = parseInt( + nodeToEdit.originalNodeObject.extra_data.days, + 10 + ); + } + + return initialValues; + } + if (!launchConfig || launchConfig === {}) { return initialValues; } @@ -186,7 +215,6 @@ const getNodeToEditDefaultValues = ( export default function useWorkflowNodeSteps( launchConfig, surveyConfig, - resource, askLinkType, resourceDefaultCredentials @@ -202,6 +230,7 @@ export default function useWorkflowNodeSteps( const steps = [ useRunTypeStep(askLinkType), useNodeTypeStep(launchConfig), + useDaysToKeepStep(), useInventoryStep(launchConfig, resource, visited), useCredentialsStep(launchConfig, resource, resourceDefaultCredentials), useOtherPromptsStep(launchConfig, resource), @@ -213,7 +242,6 @@ export default function useWorkflowNodeSteps( steps.push( usePreviewStep( launchConfig, - resource, surveyConfig, hasErrors, diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/shared/WorkflowJobTemplateVisualizerUtils.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/shared/WorkflowJobTemplateVisualizerUtils.js index eba12a84d8..7fe8e11a44 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/shared/WorkflowJobTemplateVisualizerUtils.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/shared/WorkflowJobTemplateVisualizerUtils.js @@ -2,6 +2,7 @@ import { InventorySourcesAPI, JobTemplatesAPI, ProjectsAPI, + SystemJobTemplatesAPI, WorkflowJobTemplatesAPI, } from 'api'; @@ -23,6 +24,9 @@ export default function getNodeType(node) { case 'workflow_approval_template': case 'workflow_approval': return ['approval', null]; + case 'system_job_template': + case 'system_job': + return ['system_job_template', SystemJobTemplatesAPI]; default: return [null, null]; } diff --git a/awx/ui_next/src/util/validators.js b/awx/ui_next/src/util/validators.js index 82e9b202fd..906efd22e2 100644 --- a/awx/ui_next/src/util/validators.js +++ b/awx/ui_next/src/util/validators.js @@ -57,6 +57,12 @@ export function minLength(min) { export function minMaxValue(min, max) { return (value) => { + if (!Number.isFinite(min) && value > max) { + return t`This field must be a number and have a value less than ${max}`; + } + if (!Number.isFinite(max) && value < min) { + return t`This field must be a number and have a value greater than ${min}`; + } if (value < min || value > max) { return t`This field must be a number and have a value between ${min} and ${max}`; }