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} >