mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
Merge pull request #10572 from mabashian/790-mgt-jobs-workflow
Add UI support for management jobs in workflows
This commit is contained in:
commit
b349774f92
@ -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);
|
||||
}
|
||||
|
||||
@ -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' && (
|
||||
<VariablesDetail
|
||||
label={t`Variables`}
|
||||
rows={4}
|
||||
value={overrides.extra_vars}
|
||||
name="extra_vars"
|
||||
/>
|
||||
)}
|
||||
</DetailList>
|
||||
|
||||
{hasPromptData(launchConfig) && hasOverrides && (
|
||||
<>
|
||||
<PromptTitle headingLevel="h2">{t`Prompted Values`}</PromptTitle>
|
||||
<PromptDivider />
|
||||
<PromptDetailList aria-label={t`Prompt Overrides`}>
|
||||
{launchConfig.ask_job_type_on_launch && (
|
||||
<Detail
|
||||
label={t`Job Type`}
|
||||
value={toTitleCase(overrides.job_type)}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_credential_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={t`Credentials`}
|
||||
rows={4}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={overrides.credentials.length}
|
||||
>
|
||||
{overrides.credentials.map((cred) => (
|
||||
<CredentialChip
|
||||
key={cred.id}
|
||||
credential={cred}
|
||||
isReadOnly
|
||||
/>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_inventory_on_launch && (
|
||||
<Detail label={t`Inventory`} value={overrides.inventory?.name} />
|
||||
)}
|
||||
{launchConfig.ask_scm_branch_on_launch && (
|
||||
<Detail
|
||||
label={t`Source Control Branch`}
|
||||
value={overrides.scm_branch}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_limit_on_launch && (
|
||||
<Detail label={t`Limit`} value={overrides.limit} />
|
||||
)}
|
||||
{Object.prototype.hasOwnProperty.call(overrides, 'verbosity') &&
|
||||
launchConfig.ask_verbosity_on_launch ? (
|
||||
<Detail
|
||||
label={t`Verbosity`}
|
||||
value={VERBOSITY[overrides.verbosity]}
|
||||
/>
|
||||
) : null}
|
||||
{launchConfig.ask_tags_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={t`Job Tags`}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={
|
||||
!overrides.job_tags || overrides.job_tags === ''
|
||||
? 0
|
||||
: overrides.job_tags.split(',').length
|
||||
}
|
||||
>
|
||||
{overrides.job_tags.length > 0 &&
|
||||
overrides.job_tags.split(',').map((jobTag) => (
|
||||
<Chip key={jobTag} isReadOnly>
|
||||
{jobTag}
|
||||
</Chip>
|
||||
{details?.type !== 'system_job_template' &&
|
||||
hasPromptData(launchConfig) &&
|
||||
hasOverrides && (
|
||||
<>
|
||||
<PromptTitle headingLevel="h2">{t`Prompted Values`}</PromptTitle>
|
||||
<PromptDivider />
|
||||
<PromptDetailList aria-label={t`Prompt Overrides`}>
|
||||
{launchConfig.ask_job_type_on_launch && (
|
||||
<Detail
|
||||
label={t`Job Type`}
|
||||
value={toTitleCase(overrides.job_type)}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_credential_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={t`Credentials`}
|
||||
rows={4}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={overrides.credentials.length}
|
||||
>
|
||||
{overrides.credentials.map((cred) => (
|
||||
<CredentialChip
|
||||
key={cred.id}
|
||||
credential={cred}
|
||||
isReadOnly
|
||||
/>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_skip_tags_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={t`Skip Tags`}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={
|
||||
!overrides.skip_tags || overrides.skip_tags === ''
|
||||
? 0
|
||||
: overrides.skip_tags.split(',').length
|
||||
}
|
||||
>
|
||||
{overrides.skip_tags.length > 0 &&
|
||||
overrides.skip_tags.split(',').map((skipTag) => (
|
||||
<Chip key={skipTag} isReadOnly>
|
||||
{skipTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_diff_mode_on_launch && (
|
||||
<Detail
|
||||
label={t`Show Changes`}
|
||||
value={overrides.diff_mode === true ? t`On` : t`Off`}
|
||||
/>
|
||||
)}
|
||||
{(launchConfig.survey_enabled ||
|
||||
launchConfig.ask_variables_on_launch) && (
|
||||
<VariablesDetail
|
||||
label={t`Variables`}
|
||||
rows={4}
|
||||
value={overrides.extra_vars}
|
||||
name="extra_vars"
|
||||
/>
|
||||
)}
|
||||
</PromptDetailList>
|
||||
</>
|
||||
)}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_inventory_on_launch && (
|
||||
<Detail
|
||||
label={t`Inventory`}
|
||||
value={overrides.inventory?.name}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_scm_branch_on_launch && (
|
||||
<Detail
|
||||
label={t`Source Control Branch`}
|
||||
value={overrides.scm_branch}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_limit_on_launch && (
|
||||
<Detail label={t`Limit`} value={overrides.limit} />
|
||||
)}
|
||||
{Object.prototype.hasOwnProperty.call(overrides, 'verbosity') &&
|
||||
launchConfig.ask_verbosity_on_launch ? (
|
||||
<Detail
|
||||
label={t`Verbosity`}
|
||||
value={VERBOSITY[overrides.verbosity]}
|
||||
/>
|
||||
) : null}
|
||||
{launchConfig.ask_tags_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={t`Job Tags`}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={
|
||||
!overrides.job_tags || overrides.job_tags === ''
|
||||
? 0
|
||||
: overrides.job_tags.split(',').length
|
||||
}
|
||||
>
|
||||
{overrides.job_tags.length > 0 &&
|
||||
overrides.job_tags.split(',').map((jobTag) => (
|
||||
<Chip key={jobTag} isReadOnly>
|
||||
{jobTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_skip_tags_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={t`Skip Tags`}
|
||||
value={
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={
|
||||
!overrides.skip_tags || overrides.skip_tags === ''
|
||||
? 0
|
||||
: overrides.skip_tags.split(',').length
|
||||
}
|
||||
>
|
||||
{overrides.skip_tags.length > 0 &&
|
||||
overrides.skip_tags.split(',').map((skipTag) => (
|
||||
<Chip key={skipTag} isReadOnly>
|
||||
{skipTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{launchConfig.ask_diff_mode_on_launch && (
|
||||
<Detail
|
||||
label={t`Show Changes`}
|
||||
value={overrides.diff_mode === true ? t`On` : t`Off`}
|
||||
/>
|
||||
)}
|
||||
{(launchConfig.survey_enabled ||
|
||||
launchConfig.ask_variables_on_launch) && (
|
||||
<VariablesDetail
|
||||
label={t`Variables`}
|
||||
rows={4}
|
||||
value={overrides.extra_vars}
|
||||
name="extra_vars"
|
||||
/>
|
||||
)}
|
||||
</PromptDetailList>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -103,6 +103,10 @@ function WorkflowLegend() {
|
||||
<NodeTypeLetter>P</NodeTypeLetter>
|
||||
<span>{t`Project Sync`}</span>
|
||||
</li>
|
||||
<li>
|
||||
<NodeTypeLetter>M</NodeTypeLetter>
|
||||
<span>{t`Management Job`}</span>
|
||||
</li>
|
||||
<li>
|
||||
<NodeTypeLetter>
|
||||
<PauseIcon />
|
||||
|
||||
@ -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 = '';
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -41,7 +41,7 @@ function ManagementJob({ setBreadcrumb }) {
|
||||
page_size: 1,
|
||||
role_level: 'notification_admin_role',
|
||||
}),
|
||||
]).then(([systemJobTemplate, notificationRoles]) => ({
|
||||
]).then(([{ data: systemJobTemplate }, notificationRoles]) => ({
|
||||
systemJobTemplate,
|
||||
notificationRoles,
|
||||
})),
|
||||
|
||||
@ -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 (
|
||||
<Form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
name="daysToKeep"
|
||||
id="days-to-keep"
|
||||
isRequired
|
||||
validate={combine(validators)}
|
||||
validated={!(meta.touched && meta.error) ? 'default' : 'error'}
|
||||
label={t`Days of data to be retained`}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default DaysToKeepStep;
|
||||
@ -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(
|
||||
<Formik initialValues={{ daysToKeep: 30 }}>
|
||||
<DaysToKeepStep />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -44,6 +44,11 @@ function NodeEditModal() {
|
||||
|
||||
node.launchConfig = config;
|
||||
}
|
||||
if (nodeType === 'system_job_template') {
|
||||
node.promptValues = {
|
||||
extra_data: values?.extra_data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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' && (
|
||||
<SystemJobTemplatesList
|
||||
nodeResource={nodeResourceField.value}
|
||||
onUpdateNodeResource={nodeResourceHelpers.setValue}
|
||||
/>
|
||||
)}
|
||||
{nodeTypeField.value === 'workflow_job_template' && (
|
||||
<WorkflowJobTemplatesList
|
||||
nodeResource={nodeResourceField.value}
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { SystemJobTemplatesAPI } from 'api';
|
||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import DataListToolbar from 'components/DataListToolbar';
|
||||
import CheckboxListItem from 'components/CheckboxListItem';
|
||||
import PaginatedTable, {
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
} from 'components/PaginatedTable';
|
||||
|
||||
const QS_CONFIG = getQSConfig('system-job-templates', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function SystemJobTemplatesList({ nodeResource, onUpdateNodeResource }) {
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
result: {
|
||||
systemJobTemplates,
|
||||
count,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
},
|
||||
error,
|
||||
isLoading,
|
||||
request: fetchWorkflowJobTemplates,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
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 (
|
||||
<PaginatedTable
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
itemCount={count}
|
||||
items={systemJobTemplates}
|
||||
qsConfig={QS_CONFIG}
|
||||
headerRow={
|
||||
<HeaderRow isExpandable={false} qsConfig={QS_CONFIG}>
|
||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||
</HeaderRow>
|
||||
}
|
||||
renderRow={(item, index) => (
|
||||
<CheckboxListItem
|
||||
rowIndex={index}
|
||||
isSelected={!!(nodeResource && nodeResource.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => onUpdateNodeResource(item)}
|
||||
onDeselect={() => onUpdateNodeResource(null)}
|
||||
isRadio
|
||||
/>
|
||||
)}
|
||||
renderToolbar={(props) => <DataListToolbar {...props} fillWidth />}
|
||||
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;
|
||||
@ -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(
|
||||
<SystemJobTemplatesList
|
||||
nodeResource={nodeResource}
|
||||
onUpdateNodeResource={onUpdateNodeResource}
|
||||
/>
|
||||
);
|
||||
});
|
||||
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(
|
||||
<SystemJobTemplatesList
|
||||
nodeResource={nodeResource}
|
||||
onUpdateNodeResource={onUpdateNodeResource}
|
||||
/>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -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)
|
||||
);
|
||||
|
||||
@ -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: (
|
||||
<StepName hasErrors={!!daysToKeepMeta.error} id="days-to-keep-step">
|
||||
{t`Days to keep`}
|
||||
</StepName>
|
||||
),
|
||||
component: <DaysToKeepStep />,
|
||||
enableNext: !daysToKeepMeta.error,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user