diff --git a/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx b/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx
index d0529ab483..2bc3ca1940 100644
--- a/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx
+++ b/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx
@@ -2,13 +2,13 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
-import Jobs from './Jobs';
-
+import mockJobSettings from '../shared/data.jobSettings.json';
import { SettingsAPI } from '../../../api';
+import Jobs from './Jobs';
jest.mock('../../../api/models/Settings');
SettingsAPI.readCategory.mockResolvedValue({
- data: {},
+ data: mockJobSettings,
});
describe('', () => {
diff --git a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.jsx b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.jsx
index 7ae08c9276..143c805a01 100644
--- a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.jsx
+++ b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.jsx
@@ -1,25 +1,242 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
-import { withI18n } from '@lingui/react';
-import { t } from '@lingui/macro';
-import { Button } from '@patternfly/react-core';
-import { CardBody, CardActionsRow } from '../../../../components/Card';
+import React, { useCallback, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { Formik } from 'formik';
+import { Form } from '@patternfly/react-core';
+import { CardBody } from '../../../../components/Card';
+import ContentError from '../../../../components/ContentError';
+import ContentLoading from '../../../../components/ContentLoading';
+import { FormSubmitError } from '../../../../components/FormField';
+import { FormColumnLayout } from '../../../../components/FormLayout';
+import { useSettings } from '../../../../contexts/Settings';
+import {
+ BooleanField,
+ InputField,
+ ObjectField,
+ RevertAllAlert,
+ RevertFormActionGroup,
+} from '../../shared';
+import useModal from '../../../../util/useModal';
+import useRequest from '../../../../util/useRequest';
+import { formatJson } from '../../shared/settingUtils';
+import { SettingsAPI } from '../../../../api';
+
+function JobsEdit() {
+ const history = useHistory();
+ const { isModalOpen, toggleModal, closeModal } = useModal();
+ const { PUT: options } = useSettings();
+
+ const { isLoading, error, request: fetchJobs, result: jobs } = useRequest(
+ useCallback(async () => {
+ const { data } = await SettingsAPI.readCategory('jobs');
+ const {
+ ALLOW_JINJA_IN_EXTRA_VARS,
+ AWX_ISOLATED_KEY_GENERATION,
+ AWX_ISOLATED_PRIVATE_KEY,
+ AWX_ISOLATED_PUBLIC_KEY,
+ EVENT_STDOUT_MAX_BYTES_DISPLAY,
+ STDOUT_MAX_BYTES_DISPLAY,
+ ...jobsData
+ } = data;
+ const mergedData = {};
+ Object.keys(jobsData).forEach(key => {
+ if (!options[key]) {
+ return;
+ }
+ mergedData[key] = options[key];
+ mergedData[key].value = jobsData[key];
+ });
+
+ return mergedData;
+ }, [options]),
+ null
+ );
+
+ useEffect(() => {
+ fetchJobs();
+ }, [fetchJobs]);
+
+ const { error: submitError, request: submitForm } = useRequest(
+ useCallback(
+ async values => {
+ await SettingsAPI.updateAll(values);
+ history.push('/settings/jobs/details');
+ },
+ [history]
+ ),
+ null
+ );
+
+ const handleSubmit = async form => {
+ await submitForm({
+ ...form,
+ AD_HOC_COMMANDS: formatJson(form.AD_HOC_COMMANDS),
+ AWX_PROOT_SHOW_PATHS: formatJson(form.AWX_PROOT_SHOW_PATHS),
+ AWX_PROOT_HIDE_PATHS: formatJson(form.AWX_PROOT_HIDE_PATHS),
+ AWX_ANSIBLE_CALLBACK_PLUGINS: formatJson(
+ form.AWX_ANSIBLE_CALLBACK_PLUGINS
+ ),
+ AWX_TASK_ENV: formatJson(form.AWX_TASK_ENV),
+ });
+ };
+
+ const handleRevertAll = async () => {
+ const defaultValues = {};
+ Object.entries(jobs).forEach(([key, value]) => {
+ defaultValues[key] = value.default;
+ });
+ await submitForm(defaultValues);
+ closeModal();
+ };
+
+ const handleCancel = () => {
+ history.push('/settings/jobs/details');
+ };
+
+ const initialValues = fields =>
+ Object.keys(fields).reduce((acc, key) => {
+ if (fields[key].type === 'list' || fields[key].type === 'nested object') {
+ const emptyDefault = fields[key].type === 'list' ? '[]' : '{}';
+ acc[key] = fields[key].value
+ ? JSON.stringify(fields[key].value, null, 2)
+ : emptyDefault;
+ } else {
+ acc[key] = fields[key].value ?? '';
+ }
+ return acc;
+ }, {});
-function JobsEdit({ i18n }) {
return (
- {i18n._(t`Edit form coming soon :)`)}
-
-
-
+ {isLoading && }
+ {!isLoading && error && }
+ {!isLoading && jobs && (
+
+ {formik => {
+ return (
+
+ );
+ }}
+
+ )}
);
}
-export default withI18n()(JobsEdit);
+export default JobsEdit;
diff --git a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx
index 06f4fb2f12..14b2fb11ad 100644
--- a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx
+++ b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx
@@ -1,16 +1,127 @@
import React from 'react';
-import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../../testUtils/enzymeHelpers';
+import mockAllOptions from '../../shared/data.allSettingOptions.json';
+import mockJobSettings from '../../shared/data.jobSettings.json';
+import mockDefaultJobSettings from './data.defaultJobSettings.json';
+import { SettingsProvider } from '../../../../contexts/Settings';
+import { SettingsAPI } from '../../../../api';
import JobsEdit from './JobsEdit';
+jest.mock('../../../../api/models/Settings');
+SettingsAPI.updateAll.mockResolvedValue({});
+SettingsAPI.readCategory.mockResolvedValue({
+ data: mockJobSettings,
+});
+
describe('', () => {
let wrapper;
- beforeEach(() => {
- wrapper = mountWithContexts();
- });
+ let history;
+
afterEach(() => {
wrapper.unmount();
+ jest.clearAllMocks();
});
+
+ beforeEach(async () => {
+ history = createMemoryHistory({
+ initialEntries: ['/settings/jobs/edit'],
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
test('initially renders without crashing', () => {
expect(wrapper.find('JobsEdit').length).toBe(1);
});
+
+ test('should successfully send default values to api on form revert all', async () => {
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
+ await act(async () => {
+ wrapper
+ .find('button[aria-label="Revert all to default"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
+ await act(async () => {
+ wrapper
+ .find('RevertAllAlert button[aria-label="Confirm revert all"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith(mockDefaultJobSettings);
+ });
+
+ test('should successfully send request to api on form submission', async () => {
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ const {
+ ALLOW_JINJA_IN_EXTRA_VARS,
+ AWX_ISOLATED_KEY_GENERATION,
+ AWX_ISOLATED_PRIVATE_KEY,
+ AWX_ISOLATED_PUBLIC_KEY,
+ EVENT_STDOUT_MAX_BYTES_DISPLAY,
+ STDOUT_MAX_BYTES_DISPLAY,
+ ...jobRequest
+ } = mockJobSettings;
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith(jobRequest);
+ });
+
+ test('should display error message on unsuccessful submission', async () => {
+ const error = {
+ response: {
+ data: { detail: 'An error occurred' },
+ },
+ };
+ SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
+ expect(wrapper.find('FormSubmitError').length).toBe(0);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ wrapper.update();
+ expect(wrapper.find('FormSubmitError').length).toBe(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ });
+
+ test('should navigate to job settings detail when cancel is clicked', async () => {
+ await act(async () => {
+ wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
+ });
+ expect(history.location.pathname).toEqual('/settings/jobs/details');
+ });
+
+ test('should display ContentError on throw', async () => {
+ SettingsAPI.readCategory.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.find('ContentError').length).toBe(1);
+ });
});
diff --git a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json
new file mode 100644
index 0000000000..70c73869d7
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json
@@ -0,0 +1,48 @@
+{
+ "AD_HOC_COMMANDS": [
+ "command",
+ "shell",
+ "yum",
+ "apt",
+ "apt_key",
+ "apt_repository",
+ "apt_rpm",
+ "service",
+ "group",
+ "user",
+ "mount",
+ "ping",
+ "selinux",
+ "setup",
+ "win_ping",
+ "win_service",
+ "win_updates",
+ "win_group",
+ "win_user"
+ ],
+ "ANSIBLE_FACT_CACHE_TIMEOUT": 0,
+ "AWX_ANSIBLE_CALLBACK_PLUGINS": [],
+ "AWX_COLLECTIONS_ENABLED": true,
+ "AWX_ISOLATED_CHECK_INTERVAL": 1,
+ "AWX_ISOLATED_CONNECTION_TIMEOUT": 10,
+ "AWX_ISOLATED_HOST_KEY_CHECKING": false,
+ "AWX_ISOLATED_LAUNCH_TIMEOUT": 600,
+ "AWX_PROOT_BASE_PATH": "/tmp",
+ "AWX_PROOT_ENABLED": true,
+ "AWX_PROOT_HIDE_PATHS": [],
+ "AWX_PROOT_SHOW_PATHS": [],
+ "AWX_RESOURCE_PROFILING_CPU_POLL_INTERVAL": 0.25,
+ "AWX_RESOURCE_PROFILING_ENABLED": false,
+ "AWX_RESOURCE_PROFILING_MEMORY_POLL_INTERVAL": 0.25,
+ "AWX_RESOURCE_PROFILING_PID_POLL_INTERVAL": 0.25,
+ "AWX_ROLES_ENABLED": true,
+ "AWX_SHOW_PLAYBOOK_LINKS": false,
+ "AWX_TASK_ENV": {},
+ "DEFAULT_INVENTORY_UPDATE_TIMEOUT": 0,
+ "DEFAULT_JOB_TIMEOUT": 0,
+ "DEFAULT_PROJECT_UPDATE_TIMEOUT": 0,
+ "GALAXY_IGNORE_CERTS": false,
+ "MAX_FORKS": 200,
+ "PROJECT_UPDATE_VVV": false,
+ "SCHEDULE_MAX_JOBS": 10
+}
\ No newline at end of file
diff --git a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
index 7338efb8a2..59474acd8c 100644
--- a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
+++ b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
@@ -195,7 +195,7 @@ const InputField = withI18n()(
return config ? (
{
expect(wrapper.find('TextInputBase').prop('value')).toEqual('foo');
});
+ test('InputField should revert to expected default value', async () => {
+ const wrapper = mountWithContexts(
+
+ {() => (
+
+ )}
+
+ );
+ expect(wrapper.find('TextInputBase')).toHaveLength(1);
+ expect(wrapper.find('TextInputBase').prop('value')).toEqual(5);
+ await act(async () => {
+ wrapper.find('button[aria-label="Revert"]').invoke('onClick')();
+ });
+ wrapper.update();
+ expect(wrapper.find('TextInputBase').prop('value')).toEqual(0);
+ });
+
test('TextAreaField renders the expected content', async () => {
const wrapper = mountWithContexts(