From 4e665ca77f34fed4085df7cc0580a02f9f466b27 Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 17 Aug 2022 10:57:30 -0400 Subject: [PATCH] Change ask_job_slicing_on_launch to ask_job_slice_count_on_launch to match api Adds support for prompting labels on launch in the UI Fix execution environment prompting in UI Round out support for prompting all the things on JT launch Adds timeout to job details Adds fetchAllLabels to JT/WFJT data models Moves labels methods out to a mixin so they can be shared across JTs/WFJTs/Schedules Fixes bug where ee was not being sent on launch Adds the ability to prompt for ee's, ig's, labels, timeout and job slicing to schedules Fixes bug where saving schedule form without opening the prompt would throw errors Adds support for IGs and labels to workflow node prompting Adds support for label prompting to node modal Fix job template form tests --- awx/ui/src/api/mixins/Labels.mixin.js | 35 ++++++ awx/ui/src/api/models/JobTemplates.js | 3 +- awx/ui/src/api/models/Schedules.js | 3 +- awx/ui/src/api/models/WorkflowJobTemplates.js | 5 +- .../components/LaunchButton/LaunchButton.js | 19 +++- .../components/LaunchPrompt/LaunchPrompt.js | 63 ++++++++++- .../steps/ExecutionEnvironmentStep.js | 2 +- .../LaunchPrompt/steps/InstanceGroupsStep.js | 106 ++++++++++++++++++ .../LaunchPrompt/steps/OtherPromptsStep.js | 28 ++++- .../steps/useExecutionEnvironmentStep.js | 5 +- .../steps/useInstanceGroupsStep.js | 45 ++++++++ .../LaunchPrompt/steps/useOtherPromptsStep.js | 14 ++- .../components/LaunchPrompt/useLaunchSteps.js | 12 +- .../components/PromptDetail/PromptDetail.js | 4 +- .../Schedule/ScheduleAdd/ScheduleAdd.js | 59 +++++++++- .../Schedule/ScheduleDetail/ScheduleDetail.js | 6 +- .../Schedule/ScheduleEdit/ScheduleEdit.js | 59 +++++++++- .../Schedule/shared/ScheduleForm.js | 72 ++++++++---- .../shared/SchedulePromptableFields.js | 4 +- .../Schedule/shared/useSchedulePromptSteps.js | 13 ++- awx/ui/src/screens/Job/JobDetail/JobDetail.js | 10 ++ .../JobTemplateAdd/JobTemplateAdd.test.js | 6 + .../JobTemplateEdit/JobTemplateEdit.test.js | 6 + .../Modals/NodeModals/NodeModal.js | 27 ++++- .../Modals/NodeModals/NodeViewModal.js | 2 +- .../Modals/NodeModals/useWorkflowNodeSteps.js | 12 +- .../Template/shared/JobTemplateForm.js | 5 +- 27 files changed, 556 insertions(+), 69 deletions(-) create mode 100644 awx/ui/src/api/mixins/Labels.mixin.js create mode 100644 awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js create mode 100644 awx/ui/src/components/LaunchPrompt/steps/useInstanceGroupsStep.js 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} >