diff --git a/CHANGELOG.md b/CHANGELOG.md
index d10327b2fb..a5c527b004 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ This is a list of high-level changes for each release of AWX. A full list of com
- Playbook, credential type, and inventory file inputs now support type-ahead and manual type-in! https://github.com/ansible/awx/pull/9120
- Added ability to relaunch against failed hosts: https://github.com/ansible/awx/pull/9225
- Added pending workflow approval count to the application header https://github.com/ansible/awx/pull/9334
+- Added user interface for management jobs: https://github.com/ansible/awx/pull/9224
# 17.0.1 (January 26, 2021)
- Fixed pgdocker directory permissions issue with Local Docker installer: https://github.com/ansible/awx/pull/9152
diff --git a/awx/ui_next/src/api/index.js b/awx/ui_next/src/api/index.js
index cddf01e259..3160ebd907 100644
--- a/awx/ui_next/src/api/index.js
+++ b/awx/ui_next/src/api/index.js
@@ -29,6 +29,7 @@ import Root from './models/Root';
import Schedules from './models/Schedules';
import Settings from './models/Settings';
import SystemJobs from './models/SystemJobs';
+import SystemJobTemplates from './models/SystemJobTemplates';
import Teams from './models/Teams';
import Tokens from './models/Tokens';
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
@@ -71,6 +72,7 @@ const RootAPI = new Root();
const SchedulesAPI = new Schedules();
const SettingsAPI = new Settings();
const SystemJobsAPI = new SystemJobs();
+const SystemJobTemplatesAPI = new SystemJobTemplates();
const TeamsAPI = new Teams();
const TokensAPI = new Tokens();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
@@ -114,6 +116,7 @@ export {
SchedulesAPI,
SettingsAPI,
SystemJobsAPI,
+ SystemJobTemplatesAPI,
TeamsAPI,
TokensAPI,
UnifiedJobTemplatesAPI,
diff --git a/awx/ui_next/src/api/models/Jobs.js b/awx/ui_next/src/api/models/Jobs.js
index fc9bbb2334..db28e172b6 100644
--- a/awx/ui_next/src/api/models/Jobs.js
+++ b/awx/ui_next/src/api/models/Jobs.js
@@ -9,8 +9,8 @@ const getBaseURL = type => {
case 'project':
case 'project_update':
return '/project_updates/';
- case 'system':
- case 'system_job':
+ case 'management':
+ case 'management_job':
return '/system_jobs/';
case 'inventory':
case 'inventory_update':
diff --git a/awx/ui_next/src/api/models/SystemJobTemplates.js b/awx/ui_next/src/api/models/SystemJobTemplates.js
new file mode 100644
index 0000000000..5e8b395821
--- /dev/null
+++ b/awx/ui_next/src/api/models/SystemJobTemplates.js
@@ -0,0 +1,24 @@
+import Base from '../Base';
+import NotificationsMixin from '../mixins/Notifications.mixin';
+import SchedulesMixin from '../mixins/Schedules.mixin';
+
+const Mixins = SchedulesMixin(NotificationsMixin(Base));
+
+class SystemJobTemplates extends Mixins {
+ constructor(http) {
+ super(http);
+ 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);
+ }
+}
+
+export default SystemJobTemplates;
diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx
index bd10bf6467..50f31400e5 100644
--- a/awx/ui_next/src/components/JobList/JobListItem.jsx
+++ b/awx/ui_next/src/components/JobList/JobListItem.jsx
@@ -32,7 +32,7 @@ function JobListItem({
inventory_update: i18n._(t`Inventory Sync`),
job: i18n._(t`Playbook Run`),
ad_hoc_command: i18n._(t`Command`),
- management_job: i18n._(t`Management Job`),
+ system_job: i18n._(t`Management Job`),
workflow_job: i18n._(t`Workflow Job`),
};
diff --git a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx
index f800b33217..bf70c2acc6 100644
--- a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx
+++ b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx
@@ -37,6 +37,7 @@ function PaginatedTable({
showPageSizeOptions,
i18n,
renderToolbar,
+ emptyContentMessage,
}) {
const history = useHistory();
@@ -73,9 +74,6 @@ function PaginatedTable({
const queryParams = parseQueryString(qsConfig, history.location.search);
const dataListLabel = i18n._(t`${pluralizedItemName} List`);
- const emptyContentMessage = i18n._(
- t`Please add ${pluralizedItemName} to populate this list `
- );
const emptyContentTitle = i18n._(t`No ${pluralizedItemName} Found `);
let Content;
@@ -85,7 +83,13 @@ function PaginatedTable({
Content = ;
} else if (items.length <= 0) {
Content = (
-
+
);
} else {
Content = (
diff --git a/awx/ui_next/src/components/Schedule/Schedule.jsx b/awx/ui_next/src/components/Schedule/Schedule.jsx
index e1e59c5d85..d0243ac0bd 100644
--- a/awx/ui_next/src/components/Schedule/Schedule.jsx
+++ b/awx/ui_next/src/components/Schedule/Schedule.jsx
@@ -25,6 +25,7 @@ function Schedule({
resource,
launchConfig,
surveyConfig,
+ hasDaysToKeepField,
}) {
const { scheduleId } = useParams();
@@ -69,7 +70,7 @@ function Schedule({
},
];
- if (isLoading) {
+ if (isLoading || !schedule?.summary_fields?.unified_job_template?.id) {
return ;
}
@@ -95,6 +96,7 @@ function Schedule({
if (!pathname.includes('schedules/') || pathname.endsWith('edit')) {
showCardHeader = false;
}
+
return (
<>
{showCardHeader && }
@@ -107,6 +109,7 @@ function Schedule({
{schedule && [
-
+
,
]}
diff --git a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx
index 81f42f5b72..2c8836f00e 100644
--- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx
+++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx
@@ -15,7 +15,14 @@ import mergeExtraVars from '../../../util/prompt/mergeExtraVars';
import getSurveyValues from '../../../util/prompt/getSurveyValues';
import { getAddedAndRemoved } from '../../../util/lists';
-function ScheduleAdd({ i18n, resource, apiModel, launchConfig, surveyConfig }) {
+function ScheduleAdd({
+ i18n,
+ resource,
+ apiModel,
+ launchConfig,
+ surveyConfig,
+ hasDaysToKeepField,
+}) {
const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory();
const location = useLocation();
@@ -70,13 +77,22 @@ function ScheduleAdd({ i18n, resource, apiModel, launchConfig, surveyConfig }) {
try {
const rule = new RRule(buildRuleObj(values, i18n));
+ const requestData = {
+ ...submitValues,
+ rrule: rule.toString().replace(/\n/g, ' '),
+ };
+
+ if (Object.keys(values).includes('daysToKeep')) {
+ if (requestData.extra_data) {
+ requestData.extra_data.days = values.daysToKeep;
+ } else {
+ requestData.extra_data = JSON.stringify({ days: values.daysToKeep });
+ }
+ }
const {
data: { id: scheduleId },
- } = await apiModel.createSchedule(resource.id, {
- ...submitValues,
- rrule: rule.toString().replace(/\n/g, ' '),
- });
+ } = await apiModel.createSchedule(resource.id, requestData);
if (credentials?.length > 0) {
await Promise.all(
added.map(({ id: credentialId }) =>
@@ -94,6 +110,7 @@ function ScheduleAdd({ i18n, resource, apiModel, launchConfig, surveyConfig }) {
history.push(`${pathRoot}schedules`)}
handleSubmit={handleSubmit}
submitError={formSubmitError}
diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx
index c9e4f17fa4..2c00d989b1 100644
--- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx
+++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx
@@ -26,6 +26,7 @@ import DeleteButton from '../../DeleteButton';
import ErrorDetail from '../../ErrorDetail';
import ChipGroup from '../../ChipGroup';
import { VariablesDetail } from '../../CodeMirrorInput';
+import { parseVariableField } from '../../../util/yaml';
const PromptDivider = styled(Divider)`
margin-top: var(--pf-global--spacer--lg);
@@ -42,7 +43,7 @@ const PromptDetailList = styled(DetailList)`
padding: 0px 20px;
`;
-function ScheduleDetail({ schedule, i18n, surveyConfig }) {
+function ScheduleDetail({ hasDaysToKeepField, schedule, i18n, surveyConfig }) {
const {
id,
created,
@@ -233,6 +234,16 @@ function ScheduleDetail({ schedule, i18n, surveyConfig }) {
return ;
}
+ let daysToKeep = null;
+ if (hasDaysToKeepField && extra_data) {
+ if (typeof extra_data === 'string' && extra_data !== '') {
+ daysToKeep = parseVariableField(extra_data).days;
+ }
+ if (typeof extra_data === 'object') {
+ daysToKeep = extra_data?.days;
+ }
+ }
+
return (
+ {hasDaysToKeepField ? (
+
+ ) : null}
0) {
await Promise.all([
...removed.map(({ id }) =>
@@ -111,6 +122,7 @@ function ScheduleEdit({
history.push(`${pathRoot}schedules/${schedule.id}/details`)
}
diff --git a/awx/ui_next/src/components/Schedule/Schedules.jsx b/awx/ui_next/src/components/Schedule/Schedules.jsx
index 22f429dd29..f6785d8fa8 100644
--- a/awx/ui_next/src/components/Schedule/Schedules.jsx
+++ b/awx/ui_next/src/components/Schedule/Schedules.jsx
@@ -16,10 +16,18 @@ function Schedules({
}) {
const match = useRouteMatch();
+ // For some management jobs that delete data, we want to provide an additional
+ // field on the scheduler for configuring the number of days to retain.
+ const hasDaysToKeepField = [
+ 'cleanup_activitystream',
+ 'cleanup_jobs',
+ ].includes(resource?.job_type);
+
return (
{
return null;
};
-function ScheduleFormFields({ i18n, zoneOptions }) {
+function ScheduleFormFields({ i18n, hasDaysToKeepField, zoneOptions }) {
const [startDateTime, startDateTimeMeta] = useField({
name: 'startDateTime',
validate: required(
@@ -169,6 +170,16 @@ function ScheduleFormFields({ i18n, zoneOptions }) {
{...frequency}
/>
+ {hasDaysToKeepField ? (
+
+ ) : null}
{frequency.value !== 'none' && (
@@ -184,6 +195,7 @@ function ScheduleFormFields({ i18n, zoneOptions }) {
}
function ScheduleForm({
+ hasDaysToKeepField,
handleCancel,
handleSubmit,
i18n,
@@ -344,6 +356,22 @@ function ScheduleForm({
);
};
+ if (hasDaysToKeepField) {
+ let initialDaysToKeep = 30;
+ if (schedule?.extra_data) {
+ if (
+ typeof schedule?.extra_data === 'string' &&
+ schedule?.extra_data !== ''
+ ) {
+ initialDaysToKeep = parseVariableField(schedule?.extra_data).days;
+ }
+ if (typeof schedule?.extra_data === 'object') {
+ initialDaysToKeep = schedule?.extra_data?.days;
+ }
+ }
+ initialValues.daysToKeep = initialDaysToKeep;
+ }
+
const overriddenValues = {};
if (Object.keys(schedule).length > 0) {
@@ -487,6 +515,7 @@ function ScheduleForm({