diff --git a/awx/ui/src/api/mixins/Labels.mixin.js b/awx/ui/src/api/mixins/Labels.mixin.js
new file mode 100644
index 0000000000..12e9402048
--- /dev/null
+++ b/awx/ui/src/api/mixins/Labels.mixin.js
@@ -0,0 +1,35 @@
+const LabelsMixin = (parent) =>
+ class extends parent {
+ readLabels(id, params) {
+ return this.http.get(`${this.baseUrl}${id}/labels/`, {
+ params,
+ });
+ }
+
+ readAllLabels(id) {
+ const fetchLabels = async (pageNo = 1, labels = []) => {
+ try {
+ const { data } = await this.http.get(`${this.baseUrl}${id}/labels/`, {
+ params: {
+ page: pageNo,
+ page_size: 200,
+ },
+ });
+ if (data?.next) {
+ return fetchLabels(pageNo + 1, labels.concat(data.results));
+ }
+ return Promise.resolve({
+ data: {
+ results: labels.concat(data.results),
+ },
+ });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ };
+
+ return fetchLabels();
+ }
+ };
+
+export default LabelsMixin;
diff --git a/awx/ui/src/api/models/JobTemplates.js b/awx/ui/src/api/models/JobTemplates.js
index 969ef8c8c3..7c9c6e02ae 100644
--- a/awx/ui/src/api/models/JobTemplates.js
+++ b/awx/ui/src/api/models/JobTemplates.js
@@ -1,10 +1,11 @@
import Base from '../Base';
import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
+import LabelsMixin from '../mixins/Labels.mixin';
import SchedulesMixin from '../mixins/Schedules.mixin';
class JobTemplates extends SchedulesMixin(
- InstanceGroupsMixin(NotificationsMixin(Base))
+ InstanceGroupsMixin(NotificationsMixin(LabelsMixin(Base)))
) {
constructor(http) {
super(http);
diff --git a/awx/ui/src/api/models/Schedules.js b/awx/ui/src/api/models/Schedules.js
index 40655c0349..eec5ee1396 100644
--- a/awx/ui/src/api/models/Schedules.js
+++ b/awx/ui/src/api/models/Schedules.js
@@ -1,6 +1,7 @@
import Base from '../Base';
+import LabelsMixin from '../mixins/Labels.mixin';
-class Schedules extends Base {
+class Schedules extends LabelsMixin(Base) {
constructor(http) {
super(http);
this.baseUrl = 'api/v2/schedules/';
diff --git a/awx/ui/src/api/models/WorkflowJobTemplates.js b/awx/ui/src/api/models/WorkflowJobTemplates.js
index 4ec2758653..430b8caed2 100644
--- a/awx/ui/src/api/models/WorkflowJobTemplates.js
+++ b/awx/ui/src/api/models/WorkflowJobTemplates.js
@@ -1,8 +1,11 @@
import Base from '../Base';
import SchedulesMixin from '../mixins/Schedules.mixin';
import NotificationsMixin from '../mixins/Notifications.mixin';
+import LabelsMixin from '../mixins/Labels.mixin';
-class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) {
+class WorkflowJobTemplates extends SchedulesMixin(
+ NotificationsMixin(LabelsMixin(Base))
+) {
constructor(http) {
super(http);
this.baseUrl = 'api/v2/workflow_job_templates/';
diff --git a/awx/ui/src/components/LaunchButton/LaunchButton.js b/awx/ui/src/components/LaunchButton/LaunchButton.js
index 12889ae51b..c718a5a174 100644
--- a/awx/ui/src/components/LaunchButton/LaunchButton.js
+++ b/awx/ui/src/components/LaunchButton/LaunchButton.js
@@ -1,9 +1,7 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { number, shape } from 'prop-types';
-
import { t } from '@lingui/macro';
-
import {
AdHocCommandsAPI,
InventorySourcesAPI,
@@ -27,7 +25,7 @@ function canLaunchWithoutPrompt(launchData) {
!launchData.ask_execution_environment_on_launch &&
!launchData.ask_labels_on_launch &&
!launchData.ask_forks_on_launch &&
- !launchData.ask_job_slicing_on_launch &&
+ !launchData.ask_job_slice_count_on_launch &&
!launchData.ask_timeout_on_launch &&
!launchData.ask_instance_groups_on_launch &&
!launchData.survey_enabled &&
@@ -43,6 +41,7 @@ function LaunchButton({ resource, children }) {
const [showLaunchPrompt, setShowLaunchPrompt] = useState(false);
const [launchConfig, setLaunchConfig] = useState(null);
const [surveyConfig, setSurveyConfig] = useState(null);
+ const [labels, setLabels] = useState([]);
const [isLaunching, setIsLaunching] = useState(false);
const [error, setError] = useState(null);
@@ -56,6 +55,11 @@ function LaunchButton({ resource, children }) {
resource.type === 'workflow_job_template'
? WorkflowJobTemplatesAPI.readSurvey(resource.id)
: JobTemplatesAPI.readSurvey(resource.id);
+ const readLabels =
+ resource.type === 'workflow_job_template'
+ ? WorkflowJobTemplatesAPI.readAllLabels(resource.id)
+ : JobTemplatesAPI.readAllLabels(resource.id);
+
try {
const { data: launch } = await readLaunch;
setLaunchConfig(launch);
@@ -66,6 +70,14 @@ function LaunchButton({ resource, children }) {
setSurveyConfig(data);
}
+ if (launch.ask_labels_on_launch) {
+ const {
+ data: { results },
+ } = await readLabels;
+
+ setLabels(results);
+ }
+
if (canLaunchWithoutPrompt(launch)) {
await launchWithParams({});
} else {
@@ -177,6 +189,7 @@ function LaunchButton({ resource, children }) {
launchConfig={launchConfig}
surveyConfig={surveyConfig}
resource={resource}
+ labels={labels}
onLaunch={launchWithParams}
onCancel={() => setShowLaunchPrompt(false)}
/>
diff --git a/awx/ui/src/components/LaunchPrompt/LaunchPrompt.js b/awx/ui/src/components/LaunchPrompt/LaunchPrompt.js
index b892eab4b7..290faff03f 100644
--- a/awx/ui/src/components/LaunchPrompt/LaunchPrompt.js
+++ b/awx/ui/src/components/LaunchPrompt/LaunchPrompt.js
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { ExpandableSection, Wizard } from '@patternfly/react-core';
import { t } from '@lingui/macro';
import { Formik, useFormikContext } from 'formik';
+import { LabelsAPI, OrganizationsAPI } from 'api';
import { useDismissableError } from 'hooks/useRequest';
import mergeExtraVars from 'util/prompt/mergeExtraVars';
import getSurveyValues from 'util/prompt/getSurveyValues';
@@ -15,6 +16,7 @@ function PromptModalForm({
onCancel,
onSubmit,
resource,
+ labels,
surveyConfig,
}) {
const { setFieldTouched, values } = useFormikContext();
@@ -27,9 +29,9 @@ function PromptModalForm({
visitStep,
visitAllSteps,
contentError,
- } = useLaunchSteps(launchConfig, surveyConfig, resource);
+ } = useLaunchSteps(launchConfig, surveyConfig, resource, labels);
- const handleSubmit = () => {
+ const handleSubmit = async () => {
const postValues = {};
const setValue = (key, value) => {
if (typeof value !== 'undefined' && value !== null) {
@@ -53,6 +55,61 @@ function PromptModalForm({
setValue('extra_vars', mergeExtraVars(extraVars, surveyValues));
setValue('scm_branch', values.scm_branch);
setValue('verbosity', values.verbosity);
+ setValue('timeout', values.timeout);
+ setValue('forks', values.forks);
+ setValue('job_slice_count', values.job_slice_count);
+ setValue('execution_environment', values.execution_environment?.id);
+
+ if (launchConfig.ask_instance_groups_on_launch) {
+ const instanceGroupIds = [];
+ values.instance_groups.forEach((instance_group) => {
+ instanceGroupIds.push(instance_group.id);
+ });
+ setValue('instance_groups', instanceGroupIds);
+ }
+
+ if (launchConfig.ask_labels_on_launch) {
+ const labelIds = [];
+ const newLabels = [];
+ const labelRequests = [];
+ let organizationId = resource.organization;
+ values.labels.forEach((label) => {
+ if (typeof label.id !== 'number') {
+ newLabels.push(label);
+ } else {
+ labelIds.push(label.id);
+ }
+ });
+
+ if (newLabels.length > 0) {
+ if (!organizationId) {
+ // eslint-disable-next-line no-useless-catch
+ try {
+ const {
+ data: { results },
+ } = await OrganizationsAPI.read();
+ organizationId = results[0].id;
+ } catch (err) {
+ throw err;
+ }
+ }
+ }
+
+ newLabels.forEach((label) => {
+ labelRequests.push(
+ LabelsAPI.create({
+ name: label.name,
+ organization: organizationId,
+ }).then(({ data }) => {
+ labelIds.push(data.id);
+ })
+ );
+ });
+
+ await Promise.all(labelRequests);
+
+ setValue('labels', labelIds);
+ }
onSubmit(postValues);
};
@@ -137,6 +194,7 @@ function LaunchPrompt({
onCancel,
onLaunch,
resource = {},
+ labels = [],
surveyConfig,
resourceDefaultCredentials = [],
}) {
@@ -148,6 +206,7 @@ function LaunchPrompt({
launchConfig={launchConfig}
surveyConfig={surveyConfig}
resource={resource}
+ labels={labels}
resourceDefaultCredentials={resourceDefaultCredentials}
/>
diff --git a/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js b/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js
index 14ad54c9a3..35dc12cdf5 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js
@@ -108,7 +108,7 @@ function ExecutionEnvironmentStep() {
qsConfig={QS_CONFIG}
readOnly
selectItem={helpers.setValue}
- deselectItem={() => field.onChange(null)}
+ deselectItem={() => helpers.setValue(null)}
/>
);
}
diff --git a/awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js b/awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js
new file mode 100644
index 0000000000..bd369f8d83
--- /dev/null
+++ b/awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js
@@ -0,0 +1,106 @@
+import React, { useCallback, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { t } from '@lingui/macro';
+import { useField } from 'formik';
+import { InstanceGroupsAPI } from 'api';
+import { getSearchableKeys } from 'components/PaginatedTable';
+import { getQSConfig, parseQueryString } from 'util/qs';
+import useRequest from 'hooks/useRequest';
+import useSelected from 'hooks/useSelected';
+import OptionsList from '../../OptionsList';
+import ContentLoading from '../../ContentLoading';
+import ContentError from '../../ContentError';
+
+const QS_CONFIG = getQSConfig('instance-groups', {
+ page: 1,
+ page_size: 5,
+ order_by: 'name',
+});
+
+function InstanceGroupsStep() {
+ const [field, , helpers] = useField('instance_groups');
+ const { selected, handleSelect, setSelected } = useSelected([]);
+
+ const history = useHistory();
+
+ const {
+ result: { instance_groups, count, relatedSearchableKeys, searchableKeys },
+ request: fetchInstanceGroups,
+ error,
+ isLoading,
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, history.location.search);
+ const [{ data }, actionsResponse] = await Promise.all([
+ InstanceGroupsAPI.read(params),
+ InstanceGroupsAPI.readOptions(),
+ ]);
+ return {
+ instance_groups: data.results,
+ count: data.count,
+ relatedSearchableKeys: (
+ actionsResponse?.data?.related_search_fields || []
+ ).map((val) => val.slice(0, -8)),
+ searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
+ };
+ }, [history.location]),
+ {
+ instance_groups: [],
+ count: 0,
+ relatedSearchableKeys: [],
+ searchableKeys: [],
+ }
+ );
+
+ useEffect(() => {
+ fetchInstanceGroups();
+ }, [fetchInstanceGroups]);
+
+ useEffect(() => {
+ helpers.setValue(selected);
+ }, [selected]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ if (isLoading) {
+ return ;
+ }
+ if (error) {
+ return ;
+ }
+
+ return (
+ setSelected(selectedItems)}
+ isSelectedDraggable
+ />
+ );
+}
+
+export default InstanceGroupsStep;
diff --git a/awx/ui/src/components/LaunchPrompt/steps/OtherPromptsStep.js b/awx/ui/src/components/LaunchPrompt/steps/OtherPromptsStep.js
index 623464c4f9..17d23e7710 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/OtherPromptsStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/OtherPromptsStep.js
@@ -1,9 +1,9 @@
import React from 'react';
-
import { t } from '@lingui/macro';
import { useField } from 'formik';
import { Form, FormGroup, Switch } from '@patternfly/react-core';
import styled from 'styled-components';
+import LabelSelect from '../../LabelSelect';
import FormField from '../../FormField';
import { TagMultiSelect } from '../../MultiSelect';
import AnsibleSelect from '../../AnsibleSelect';
@@ -37,6 +37,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
tooltip={t`Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch`}
/>
)}
+ {launchConfig.ask_labels_on_launch && }
{launchConfig.ask_forks_on_launch && (
)}
{launchConfig.ask_verbosity_on_launch && }
- {launchConfig.ask_job_slicing_on_launch && (
+ {launchConfig.ask_job_slice_count_on_launch && (
+ }
+ >
+ helpers.setValue(labels)}
+ createText={t`Create`}
+ onError={() => alert('error')}
+ />
+
+ );
+}
+
export default OtherPromptsStep;
diff --git a/awx/ui/src/components/LaunchPrompt/steps/useExecutionEnvironmentStep.js b/awx/ui/src/components/LaunchPrompt/steps/useExecutionEnvironmentStep.js
index 8efb3b676d..611330ad55 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/useExecutionEnvironmentStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/useExecutionEnvironmentStep.js
@@ -19,7 +19,7 @@ export default function useExecutionEnvironmentStep(launchConfig, resource) {
};
}
function getStep(launchConfig) {
- if (!launchConfig.ask_inventory_on_launch) {
+ if (!launchConfig.ask_execution_environment_on_launch) {
return null;
}
return {
@@ -40,6 +40,7 @@ function getInitialValues(launchConfig, resource) {
}
return {
- inventory: resource?.summary_fields?.execution_environment || null,
+ execution_environment:
+ resource?.summary_fields?.execution_environment || null,
};
}
diff --git a/awx/ui/src/components/LaunchPrompt/steps/useInstanceGroupsStep.js b/awx/ui/src/components/LaunchPrompt/steps/useInstanceGroupsStep.js
new file mode 100644
index 0000000000..a15b868b69
--- /dev/null
+++ b/awx/ui/src/components/LaunchPrompt/steps/useInstanceGroupsStep.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { t } from '@lingui/macro';
+import InstanceGroupsStep from './InstanceGroupsStep';
+import StepName from './StepName';
+
+const STEP_ID = 'instanceGroups';
+
+export default function useInstanceGroupsStep(
+ launchConfig,
+ resource,
+ instanceGroups
+) {
+ return {
+ step: getStep(launchConfig, resource),
+ initialValues: getInitialValues(launchConfig, instanceGroups),
+ isReady: true,
+ contentError: null,
+ hasError: false,
+ setTouched: (setFieldTouched) => {
+ setFieldTouched('instance_groups', true, false);
+ },
+ validate: () => {},
+ };
+}
+function getStep(launchConfig) {
+ if (!launchConfig.ask_instance_groups_on_launch) {
+ return null;
+ }
+ return {
+ id: STEP_ID,
+ name: {t`Instance Groups`},
+ component: ,
+ enableNext: true,
+ };
+}
+
+function getInitialValues(launchConfig, instanceGroups) {
+ if (!launchConfig.ask_instance_groups_on_launch) {
+ return {};
+ }
+
+ return {
+ instance_groups: instanceGroups || [],
+ };
+}
diff --git a/awx/ui/src/components/LaunchPrompt/steps/useOtherPromptsStep.js b/awx/ui/src/components/LaunchPrompt/steps/useOtherPromptsStep.js
index 4e3205e323..620fe8337c 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/useOtherPromptsStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/useOtherPromptsStep.js
@@ -31,9 +31,10 @@ const FIELD_NAMES = [
'timeout',
'job_slice_count',
'forks',
+ 'labels',
];
-export default function useOtherPromptsStep(launchConfig, resource) {
+export default function useOtherPromptsStep(launchConfig, resource, labels) {
const [variablesField] = useField('extra_vars');
const [variablesMode, setVariablesMode] = useState(null);
const [isTouched, setIsTouched] = useState(false);
@@ -63,7 +64,7 @@ export default function useOtherPromptsStep(launchConfig, resource) {
return {
step: getStep(launchConfig, hasError, variablesMode, handleModeChange),
- initialValues: getInitialValues(launchConfig, resource),
+ initialValues: getInitialValues(launchConfig, resource, labels),
isReady: true,
contentError: null,
hasError,
@@ -112,12 +113,12 @@ function shouldShowPrompt(launchConfig) {
launchConfig.ask_diff_mode_on_launch ||
launchConfig.ask_labels_on_launch ||
launchConfig.ask_forks_on_launch ||
- launchConfig.ask_job_slicing_on_launch ||
+ launchConfig.ask_job_slice_count_on_launch ||
launchConfig.ask_timeout_on_launch
);
}
-function getInitialValues(launchConfig, resource) {
+function getInitialValues(launchConfig, resource, labels) {
const initialValues = {};
if (!launchConfig) {
@@ -151,11 +152,14 @@ function getInitialValues(launchConfig, resource) {
if (launchConfig.ask_forks_on_launch) {
initialValues.forks = resource?.forks || 0;
}
- if (launchConfig.ask_job_slicing_on_launch) {
+ if (launchConfig.ask_job_slice_count_on_launch) {
initialValues.job_slice_count = resource?.job_slice_count || 1;
}
if (launchConfig.ask_timeout_on_launch) {
initialValues.timeout = resource?.timeout || 0;
}
+ if (launchConfig.ask_labels_on_launch) {
+ initialValues.labels = labels || [];
+ }
return initialValues;
}
diff --git a/awx/ui/src/components/LaunchPrompt/useLaunchSteps.js b/awx/ui/src/components/LaunchPrompt/useLaunchSteps.js
index a129143ae1..fda7c79854 100644
--- a/awx/ui/src/components/LaunchPrompt/useLaunchSteps.js
+++ b/awx/ui/src/components/LaunchPrompt/useLaunchSteps.js
@@ -7,6 +7,7 @@ import useExecutionEnvironmentStep from './steps/useExecutionEnvironmentStep';
import useOtherPromptsStep from './steps/useOtherPromptsStep';
import useSurveyStep from './steps/useSurveyStep';
import usePreviewStep from './steps/usePreviewStep';
+import useInstanceGroupsStep from './steps/useInstanceGroupsStep';
function showCredentialPasswordsStep(launchConfig, credentials = []) {
if (
@@ -40,7 +41,12 @@ function showCredentialPasswordsStep(launchConfig, credentials = []) {
return credentialPasswordStepRequired;
}
-export default function useLaunchSteps(launchConfig, surveyConfig, resource) {
+export default function useLaunchSteps(
+ launchConfig,
+ surveyConfig,
+ resource,
+ labels
+) {
const [visited, setVisited] = useState({});
const [isReady, setIsReady] = useState(false);
const { touched, values: formikValues } = useFormikContext();
@@ -58,7 +64,8 @@ export default function useLaunchSteps(launchConfig, surveyConfig, resource) {
visited
),
useExecutionEnvironmentStep(launchConfig, resource),
- useOtherPromptsStep(launchConfig, resource),
+ useInstanceGroupsStep(launchConfig, resource),
+ useOtherPromptsStep(launchConfig, resource, labels),
useSurveyStep(launchConfig, surveyConfig, resource, visited),
];
const { resetForm } = useFormikContext();
@@ -146,6 +153,7 @@ export default function useLaunchSteps(launchConfig, surveyConfig, resource) {
credentials: true,
credentialPasswords: true,
executionEnvironment: true,
+ instanceGroups: true,
other: true,
survey: true,
preview: true,
diff --git a/awx/ui/src/components/PromptDetail/PromptDetail.js b/awx/ui/src/components/PromptDetail/PromptDetail.js
index 6e44968349..d52767fb0b 100644
--- a/awx/ui/src/components/PromptDetail/PromptDetail.js
+++ b/awx/ui/src/components/PromptDetail/PromptDetail.js
@@ -75,7 +75,7 @@ function hasPromptData(launchData) {
launchData.ask_execution_environment_on_launch ||
launchData.ask_labels_on_launch ||
launchData.ask_forks_on_launch ||
- launchData.ask_job_slicing_on_launch ||
+ launchData.ask_job_slice_count_on_launch ||
launchData.ask_timeout_on_launch ||
launchData.ask_instance_groups_on_launch
);
@@ -341,7 +341,7 @@ function PromptDetail({
{launchConfig.ask_forks_on_launch && (
)}
- {launchConfig.ask_job_slicing_on_launch && (
+ {launchConfig.ask_job_slice_count_on_launch && (
{
const {
+ execution_environment,
+ instance_groups,
inventory,
frequency,
frequencyOptions,
@@ -72,7 +72,60 @@ function ScheduleAdd({
submitValues.inventory = inventory.id;
}
+ if (execution_environment) {
+ submitValues.execution_environment = execution_environment.id;
+ }
+
+ submitValues.instance_groups = instance_groups
+ ? instance_groups.map((s) => s.id)
+ : [];
+
try {
+ if (launchConfiguration?.ask_labels_on_launch) {
+ const labelIds = [];
+ const newLabels = [];
+ const labelRequests = [];
+ let organizationId = resource.organization;
+ if (values.labels) {
+ values.labels.forEach((label) => {
+ if (typeof label.id !== 'number') {
+ newLabels.push(label);
+ } else {
+ labelIds.push(label.id);
+ }
+ });
+ }
+
+ if (newLabels.length > 0) {
+ if (!organizationId) {
+ // eslint-disable-next-line no-useless-catch
+ try {
+ const {
+ data: { results },
+ } = await OrganizationsAPI.read();
+ organizationId = results[0].id;
+ } catch (err) {
+ throw err;
+ }
+ }
+ }
+
+ newLabels.forEach((label) => {
+ labelRequests.push(
+ LabelsAPI.create({
+ name: label.name,
+ organization: organizationId,
+ }).then(({ data }) => {
+ labelIds.push(data.id);
+ })
+ );
+ });
+
+ await Promise.all(labelRequests);
+
+ submitValues.labels = labelIds;
+ }
+
const ruleSet = buildRuleSet(values);
const requestData = {
...submitValues,
diff --git a/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js b/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js
index 7c37d03ea9..05fd0ba3cb 100644
--- a/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js
+++ b/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js
@@ -193,7 +193,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
ask_execution_environment_on_launch,
ask_labels_on_launch,
ask_forks_on_launch,
- ask_job_slicing_on_launch,
+ ask_job_slice_count_on_launch,
ask_timeout_on_launch,
survey_enabled,
} = launchData || {};
@@ -253,7 +253,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
ask_execution_environment_on_launch && execution_environment;
const showLabelsDetail = ask_labels_on_launch && labels && labels.length > 0;
const showForksDetail = ask_forks_on_launch;
- const showJobSlicingDetail = ask_job_slicing_on_launch;
+ const showJobSlicingDetail = ask_job_slice_count_on_launch;
const showTimeoutDetail = ask_timeout_on_launch;
const showPromptedFields =
@@ -468,7 +468,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
dataCy="schedule-show-changes"
/>
)}
- {ask_job_slicing_on_launch && (
+ {ask_job_slice_count_on_launch && (
)}
{showCredentialsDetail && (
diff --git a/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js b/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js
index 143a428de0..22e3ff20b1 100644
--- a/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js
+++ b/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js
@@ -1,12 +1,10 @@
import React, { useState } from 'react';
-
import { useHistory, useLocation } from 'react-router-dom';
import { shape } from 'prop-types';
import { Card } from '@patternfly/react-core';
import yaml from 'js-yaml';
-import { SchedulesAPI } from 'api';
+import { LabelsAPI, OrganizationsAPI, SchedulesAPI } from 'api';
import { getAddedAndRemoved } from 'util/lists';
-
import { parseVariableField } from 'util/yaml';
import mergeExtraVars from 'util/prompt/mergeExtraVars';
import getSurveyValues from 'util/prompt/getSurveyValues';
@@ -35,6 +33,8 @@ function ScheduleEdit({
scheduleCredentials = []
) => {
const {
+ execution_environment,
+ instance_groups,
inventory,
credentials = [],
frequency,
@@ -82,7 +82,60 @@ function ScheduleEdit({
submitValues.inventory = inventory.id;
}
+ if (execution_environment) {
+ submitValues.execution_environment = execution_environment.id;
+ }
+
+ submitValues.instance_groups = instance_groups
+ ? instance_groups.map((s) => s.id)
+ : [];
+
try {
+ if (launchConfiguration?.ask_labels_on_launch) {
+ const labelIds = [];
+ const newLabels = [];
+ const labelRequests = [];
+ let organizationId = resource.organization;
+ if (values.labels) {
+ values.labels.forEach((label) => {
+ if (typeof label.id !== 'number') {
+ newLabels.push(label);
+ } else {
+ labelIds.push(label.id);
+ }
+ });
+ }
+
+ if (newLabels.length > 0) {
+ if (!organizationId) {
+ // eslint-disable-next-line no-useless-catch
+ try {
+ const {
+ data: { results },
+ } = await OrganizationsAPI.read();
+ organizationId = results[0].id;
+ } catch (err) {
+ throw err;
+ }
+ }
+ }
+
+ newLabels.forEach((label) => {
+ labelRequests.push(
+ LabelsAPI.create({
+ name: label.name,
+ organization: organizationId,
+ }).then(({ data }) => {
+ labelIds.push(data.id);
+ })
+ );
+ });
+
+ await Promise.all(labelRequests);
+
+ submitValues.labels = labelIds;
+ }
+
const ruleSet = buildRuleSet(values);
const requestData = {
...submitValues,
diff --git a/awx/ui/src/components/Schedule/shared/ScheduleForm.js b/awx/ui/src/components/Schedule/shared/ScheduleForm.js
index 63673c4bc5..6a92fd3625 100644
--- a/awx/ui/src/components/Schedule/shared/ScheduleForm.js
+++ b/awx/ui/src/components/Schedule/shared/ScheduleForm.js
@@ -1,13 +1,12 @@
import React, { useEffect, useCallback, useState } from 'react';
import { shape, func } from 'prop-types';
-
import { DateTime } from 'luxon';
import { t } from '@lingui/macro';
import { Formik } from 'formik';
import { RRule } from 'rrule';
import { Button, Form, ActionGroup } from '@patternfly/react-core';
import { Config } from 'contexts/Config';
-import { SchedulesAPI } from 'api';
+import { JobTemplatesAPI, SchedulesAPI, WorkflowJobTemplatesAPI } from 'api';
import { dateToInputDateTime } from 'util/dates';
import useRequest from 'hooks/useRequest';
import { parseVariableField } from 'util/yaml';
@@ -31,7 +30,7 @@ const NUM_DAYS_PER_FREQUENCY = {
function ScheduleForm({
hasDaysToKeepField,
handleCancel,
- handleSubmit,
+ handleSubmit: submitSchedule,
schedule,
submitError,
resource,
@@ -55,17 +54,48 @@ function ScheduleForm({
request: loadScheduleData,
error: contentError,
isLoading: contentLoading,
- result: { zoneOptions, zoneLinks, credentials },
+ result: { zoneOptions, zoneLinks, credentials, labels },
} = useRequest(
useCallback(async () => {
const { data } = await SchedulesAPI.readZoneInfo();
let creds;
+ let allLabels;
if (schedule.id) {
- const {
- data: { results },
- } = await SchedulesAPI.readCredentials(schedule.id);
- creds = results;
+ if (
+ resource.type === 'job_template' &&
+ launchConfig.ask_credential_on_launch
+ ) {
+ const {
+ data: { results },
+ } = await SchedulesAPI.readCredentials(schedule.id);
+ creds = results;
+ }
+ if (launchConfig.ask_labels_on_launch) {
+ const {
+ data: { results },
+ } = await SchedulesAPI.readAllLabels(schedule.id);
+ allLabels = results;
+ }
+ } else {
+ if (
+ resource.type === 'job_template' &&
+ launchConfig.ask_labels_on_launch
+ ) {
+ const {
+ data: { results },
+ } = await JobTemplatesAPI.readAllLabels(resource.id);
+ allLabels = results;
+ }
+ if (
+ resource.type === 'workflow_job_template' &&
+ launchConfig.ask_labels_on_launch
+ ) {
+ const {
+ data: { results },
+ } = await WorkflowJobTemplatesAPI.readAllLabels(resource.id);
+ allLabels = results;
+ }
}
const zones = (data.zones || []).map((zone) => ({
@@ -78,13 +108,21 @@ function ScheduleForm({
zoneOptions: zones,
zoneLinks: data.links,
credentials: creds || [],
+ labels: allLabels || [],
};
- }, [schedule]),
+ }, [
+ schedule,
+ resource.id,
+ resource.type,
+ launchConfig.ask_labels_on_launch,
+ launchConfig.ask_credential_on_launch,
+ ]),
{
zonesOptions: [],
zoneLinks: {},
credentials: [],
isLoading: true,
+ labels: [],
}
);
@@ -228,7 +266,7 @@ function ScheduleForm({
launchConfig.ask_execution_environment_on_launch ||
launchConfig.ask_labels_on_launch ||
launchConfig.ask_forks_on_launch ||
- launchConfig.ask_job_slicing_on_launch ||
+ launchConfig.ask_job_slice_count_on_launch ||
launchConfig.ask_timeout_on_launch ||
launchConfig.ask_instance_groups_on_launch ||
launchConfig.survey_enabled ||
@@ -307,19 +345,6 @@ function ScheduleForm({
startTime: time,
timezone: schedule.timezone || now.zoneName,
};
- const submitSchedule = (
- values,
- launchConfiguration,
- surveyConfiguration,
- scheduleCredentials
- ) => {
- handleSubmit(
- values,
- launchConfiguration,
- surveyConfiguration,
- scheduleCredentials
- );
- };
if (hasDaysToKeepField) {
let initialDaysToKeep = 30;
@@ -469,6 +494,7 @@ function ScheduleForm({
setIsSaveDisabled(false);
}}
resourceDefaultCredentials={resourceDefaultCredentials}
+ labels={labels}
/>
)}
diff --git a/awx/ui/src/components/Schedule/shared/SchedulePromptableFields.js b/awx/ui/src/components/Schedule/shared/SchedulePromptableFields.js
index 406398806b..21a33d21cf 100644
--- a/awx/ui/src/components/Schedule/shared/SchedulePromptableFields.js
+++ b/awx/ui/src/components/Schedule/shared/SchedulePromptableFields.js
@@ -17,6 +17,7 @@ function SchedulePromptableFields({
credentials,
resource,
resourceDefaultCredentials,
+ labels,
}) {
const { setFieldTouched, values, initialValues, resetForm } =
useFormikContext();
@@ -33,7 +34,8 @@ function SchedulePromptableFields({
schedule,
resource,
credentials,
- resourceDefaultCredentials
+ resourceDefaultCredentials,
+ labels
);
const [showDescription, setShowDescription] = useState(false);
const { error, dismissError } = useDismissableError(contentError);
diff --git a/awx/ui/src/components/Schedule/shared/useSchedulePromptSteps.js b/awx/ui/src/components/Schedule/shared/useSchedulePromptSteps.js
index ef31e14d23..7644d8c277 100644
--- a/awx/ui/src/components/Schedule/shared/useSchedulePromptSteps.js
+++ b/awx/ui/src/components/Schedule/shared/useSchedulePromptSteps.js
@@ -3,6 +3,8 @@ import { useFormikContext } from 'formik';
import { t } from '@lingui/macro';
import useInventoryStep from '../../LaunchPrompt/steps/useInventoryStep';
import useCredentialsStep from '../../LaunchPrompt/steps/useCredentialsStep';
+import useExecutionEnvironmentStep from '../../LaunchPrompt/steps/useExecutionEnvironmentStep';
+import useInstanceGroupsStep from '../../LaunchPrompt/steps/useInstanceGroupsStep';
import useOtherPromptsStep from '../../LaunchPrompt/steps/useOtherPromptsStep';
import useSurveyStep from '../../LaunchPrompt/steps/useSurveyStep';
import usePreviewStep from '../../LaunchPrompt/steps/usePreviewStep';
@@ -12,9 +14,9 @@ export default function useSchedulePromptSteps(
launchConfig,
schedule,
resource,
-
scheduleCredentials,
- resourceDefaultCredentials
+ resourceDefaultCredentials,
+ labels
) {
const sourceOfValues =
(Object.keys(schedule).length > 0 && schedule) || resource;
@@ -28,7 +30,9 @@ export default function useSchedulePromptSteps(
sourceOfValues,
resourceDefaultCredentials
),
- useOtherPromptsStep(launchConfig, sourceOfValues),
+ useExecutionEnvironmentStep(launchConfig, resource),
+ useInstanceGroupsStep(launchConfig, resource),
+ useOtherPromptsStep(launchConfig, sourceOfValues, labels),
useSurveyStep(launchConfig, surveyConfig, sourceOfValues, visited),
];
@@ -37,7 +41,6 @@ export default function useSchedulePromptSteps(
steps.push(
usePreviewStep(
launchConfig,
-
resource,
surveyConfig,
hasErrors,
@@ -130,6 +133,8 @@ export default function useSchedulePromptSteps(
setVisited({
inventory: true,
credentials: true,
+ executionEnvironment: true,
+ instanceGroups: true,
other: true,
survey: true,
preview: true,
diff --git a/awx/ui/src/screens/Job/JobDetail/JobDetail.js b/awx/ui/src/screens/Job/JobDetail/JobDetail.js
index 2ffd6b6f75..d3435307cf 100644
--- a/awx/ui/src/screens/Job/JobDetail/JobDetail.js
+++ b/awx/ui/src/screens/Job/JobDetail/JobDetail.js
@@ -391,6 +391,16 @@ function JobDetail({ job, inventorySourceLabels }) {
helpText={jobHelpText.forks}
/>
)}
+ {typeof job.timeout === 'number' && (
+
+ )}
{credential && (
{
@@ -241,7 +243,7 @@ const NodeModalInner = ({ title, ...rest }) => {
const {
request: readLaunchConfigs,
error: launchConfigError,
- result: { launchConfig, surveyConfig, resourceDefaultCredentials },
+ result: { launchConfig, surveyConfig, resourceDefaultCredentials, labels },
isLoading,
} = useRequest(
useCallback(async () => {
@@ -260,9 +262,15 @@ const NodeModalInner = ({ title, ...rest }) => {
launchConfig: {},
surveyConfig: {},
resourceDefaultCredentials: [],
+ labels: [],
};
}
+ const readLabels =
+ values.nodeType === 'workflow_job_template'
+ ? WorkflowJobTemplatesAPI.readAllLabels(values.nodeResource.id)
+ : JobTemplatesAPI.readAllLabels(values.nodeResource.id);
+
const { data: launch } = await readLaunch(
values.nodeType,
values?.nodeResource?.id
@@ -291,10 +299,21 @@ const NodeModalInner = ({ title, ...rest }) => {
defaultCredentials = results;
}
+ let defaultLabels = [];
+
+ if (launch.ask_labels_on_launch) {
+ const {
+ data: { results },
+ } = await readLabels;
+
+ defaultLabels = results;
+ }
+
return {
launchConfig: launch,
surveyConfig: survey,
resourceDefaultCredentials: defaultCredentials,
+ labels: defaultLabels,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -347,11 +366,12 @@ const NodeModalInner = ({ title, ...rest }) => {
resourceDefaultCredentials={resourceDefaultCredentials}
isLaunchLoading={isLoading}
title={wizardTitle}
+ labels={labels}
/>
);
};
-const NodeModal = ({ onSave, askLinkType, title }) => {
+const NodeModal = ({ onSave, askLinkType, title, labels }) => {
const { nodeToEdit } = useContext(WorkflowStateContext);
const onSaveForm = (values, config) => {
onSave(values, config);
@@ -378,6 +398,7 @@ const NodeModal = ({ onSave, askLinkType, title }) => {
onSave={onSaveForm}
title={title}
askLinkType={askLinkType}
+ labels={labels}
/>
)}
diff --git a/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js b/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js
index 90a6790cce..181f5045eb 100644
--- a/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js
+++ b/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.js
@@ -165,7 +165,7 @@ function NodeViewModal({ readOnly }) {
if (launchConfig.ask_forks_on_launch) {
overrides.forks = originalNodeObject.forks;
}
- if (launchConfig.ask_job_slicing_on_launch) {
+ if (launchConfig.ask_job_slice_count_on_launch) {
overrides.job_slice_count = originalNodeObject.job_slice_count;
}
if (launchConfig.ask_timeout_on_launch) {
diff --git a/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js b/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js
index 91af7e6e27..9688f9c703 100644
--- a/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js
+++ b/awx/ui/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js
@@ -7,6 +7,7 @@ import useExecutionEnvironmentStep from 'components/LaunchPrompt/steps/useExecut
import useOtherPromptsStep from 'components/LaunchPrompt/steps/useOtherPromptsStep';
import useSurveyStep from 'components/LaunchPrompt/steps/useSurveyStep';
import usePreviewStep from 'components/LaunchPrompt/steps/usePreviewStep';
+import useInstanceGroupsStep from 'components/LaunchPrompt/steps/useInstanceGroupsStep';
import { WorkflowStateContext } from 'contexts/Workflow';
import { jsonToYaml } from 'util/yaml';
import { stringIsUUID } from 'util/strings';
@@ -30,7 +31,7 @@ function showPreviewStep(nodeType, launchConfig) {
launchConfig.ask_execution_environment_on_launch ||
launchConfig.ask_labels_on_launch ||
launchConfig.ask_forks_on_launch ||
- launchConfig.ask_job_slicing_on_launch ||
+ launchConfig.ask_job_slice_count_on_launch ||
launchConfig.ask_timeout_on_launch ||
launchConfig.ask_instance_groups_on_launch ||
launchConfig.survey_enabled ||
@@ -221,7 +222,7 @@ const getNodeToEditDefaultValues = (
if (launchConfig.ask_forks_on_launch) {
initialValues.forks = sourceOfValues?.forks || 0;
}
- if (launchConfig.ask_job_slicing_on_launch) {
+ if (launchConfig.ask_job_slice_count_on_launch) {
initialValues.job_slice_count = sourceOfValues?.job_slice_count || 1;
}
if (launchConfig.ask_timeout_on_launch) {
@@ -272,7 +273,8 @@ export default function useWorkflowNodeSteps(
surveyConfig,
resource,
askLinkType,
- resourceDefaultCredentials
+ resourceDefaultCredentials,
+ labels
) {
const { nodeToEdit } = useContext(WorkflowStateContext);
const {
@@ -289,7 +291,8 @@ export default function useWorkflowNodeSteps(
useInventoryStep(launchConfig, resource, visited),
useCredentialsStep(launchConfig, resource, resourceDefaultCredentials),
useExecutionEnvironmentStep(launchConfig, resource),
- useOtherPromptsStep(launchConfig, resource),
+ useInstanceGroupsStep(launchConfig, resource),
+ useOtherPromptsStep(launchConfig, resource, labels),
useSurveyStep(launchConfig, surveyConfig, resource, visited),
];
@@ -380,6 +383,7 @@ export default function useWorkflowNodeSteps(
inventory: true,
credentials: true,
executionEnvironment: true,
+ instanceGroups: true,
other: true,
survey: true,
preview: true,
diff --git a/awx/ui/src/screens/Template/shared/JobTemplateForm.js b/awx/ui/src/screens/Template/shared/JobTemplateForm.js
index f8aebf8ced..a82aaa8ad3 100644
--- a/awx/ui/src/screens/Template/shared/JobTemplateForm.js
+++ b/awx/ui/src/screens/Template/shared/JobTemplateForm.js
@@ -452,7 +452,7 @@ function JobTemplateForm({
fieldId="template-job-slicing"
label={t`Job Slicing`}
promptId="template-ask-job-slicing-on-launch"
- promptName="ask_job_slicing_on_launch"
+ promptName="ask_job_slice_count_on_launch"
tooltip={helpText.jobSlicing}
>