diff --git a/Makefile b/Makefile
index 9052b9a518..e495485b0f 100644
--- a/Makefile
+++ b/Makefile
@@ -288,6 +288,11 @@ swagger: reports
check: black
+api-lint:
+ BLACK_ARGS="--check" make black
+ flake8 awx
+ yamllint -s .
+
awx-link:
[ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/setup.py egg_info_dev
cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/python$(PYTHON_VERSION)/site-packages/awx.egg-link
diff --git a/awx/api/serializers.py b/awx/api/serializers.py
index 1f0f127fc9..12799e0d94 100644
--- a/awx/api/serializers.py
+++ b/awx/api/serializers.py
@@ -729,11 +729,12 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
if self.is_detail_view:
resolved_ee = obj.resolve_execution_environment()
- summary_fields['resolved_environment'] = {
- field: getattr(resolved_ee, field, None)
- for field in SUMMARIZABLE_FK_FIELDS['execution_environment']
- if getattr(resolved_ee, field, None) is not None
- }
+ if resolved_ee is not None:
+ summary_fields['resolved_environment'] = {
+ field: getattr(resolved_ee, field, None)
+ for field in SUMMARIZABLE_FK_FIELDS['execution_environment']
+ if getattr(resolved_ee, field, None) is not None
+ }
return summary_fields
@@ -3425,6 +3426,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
'ask_limit_on_launch',
'webhook_service',
'webhook_credential',
+ '-execution_environment',
)
def get_related(self, obj):
@@ -3451,6 +3453,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
survey_spec=self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}),
copy=self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}),
)
+ res.pop('execution_environment', None) # EEs aren't meaningful for workflows
if obj.organization:
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
if obj.webhook_credential_id:
@@ -3502,6 +3505,7 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
'allow_simultaneous',
'job_template',
'is_sliced_job',
+ '-execution_environment',
'-execution_node',
'-event_processing_finished',
'-controller_node',
@@ -3515,6 +3519,7 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
def get_related(self, obj):
res = super(WorkflowJobSerializer, self).get_related(obj)
+ res.pop('execution_environment', None) # EEs aren't meaningful for workflows
if obj.workflow_job_template:
res['workflow_job_template'] = self.reverse('api:workflow_job_template_detail', kwargs={'pk': obj.workflow_job_template.pk})
res['notifications'] = self.reverse('api:workflow_job_notifications_list', kwargs={'pk': obj.pk})
@@ -3539,7 +3544,7 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
class WorkflowJobListSerializer(WorkflowJobSerializer, UnifiedJobListSerializer):
class Meta:
- fields = ('*', '-execution_node', '-controller_node')
+ fields = ('*', '-execution_environment', '-execution_node', '-controller_node')
class WorkflowJobCancelSerializer(WorkflowJobSerializer):
diff --git a/awx/main/middleware.py b/awx/main/middleware.py
index 05e03777fb..71b9ff33d2 100644
--- a/awx/main/middleware.py
+++ b/awx/main/middleware.py
@@ -46,7 +46,7 @@ class TimingMiddleware(threading.local, MiddlewareMixin):
response['X-API-Total-Time'] = '%0.3fs' % total_time
if settings.AWX_REQUEST_PROFILE:
response['X-API-Profile-File'] = self.prof.stop()
- perf_logger.info(
+ perf_logger.debug(
f'request: {request}, response_time: {response["X-API-Total-Time"]}',
extra=dict(python_objects=dict(request=request, response=response, X_API_TOTAL_TIME=response["X-API-Total-Time"])),
)
diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py
index 585f1c6592..004e43401a 100644
--- a/awx/main/models/inventory.py
+++ b/awx/main/models/inventory.py
@@ -1225,6 +1225,10 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
def is_container_group_task(self):
return bool(self.instance_group and self.instance_group.is_container_group)
+ @property
+ def can_run_containerized(self):
+ return True
+
def _get_parent_field_name(self):
return 'inventory_source'
diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py
index 7f929c757d..45a3cae885 100644
--- a/awx/main/models/mixins.py
+++ b/awx/main/models/mixins.py
@@ -471,13 +471,6 @@ class ExecutionEnvironmentMixin(models.Model):
template = getattr(self, 'unified_job_template', None)
if template is not None and template.execution_environment is not None:
return template.execution_environment
- wf_node = getattr(self, 'unified_job_node', None)
- while wf_node is not None:
- wf_template = wf_node.workflow_job.workflow_job_template
- # NOTE: sliced workflow_jobs have a null workflow_job_template
- if wf_template and wf_template.execution_environment is not None:
- return wf_template.execution_environment
- wf_node = getattr(wf_node.workflow_job, 'unified_job_node', None)
if getattr(self, 'project_id', None) and self.project.default_environment is not None:
return self.project.default_environment
if getattr(self, 'organization_id', None) and self.organization.default_environment is not None:
diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py
index 463a98f805..b254ee41c8 100644
--- a/awx/main/models/workflow.py
+++ b/awx/main/models/workflow.py
@@ -595,6 +595,9 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
def _get_related_jobs(self):
return WorkflowJob.objects.filter(workflow_job_template=self)
+ def resolve_execution_environment(self):
+ return None # EEs are not meaningful for workflows
+
class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, WebhookMixin):
class Meta:
diff --git a/awx/ui_next/src/App.jsx b/awx/ui_next/src/App.jsx
index 88c91406c0..59a8e28cd9 100644
--- a/awx/ui_next/src/App.jsx
+++ b/awx/ui_next/src/App.jsx
@@ -12,7 +12,11 @@ import { ErrorBoundary } from 'react-error-boundary';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { Card, PageSection } from '@patternfly/react-core';
-import { ConfigProvider, useAuthorizedPath } from './contexts/Config';
+import {
+ ConfigProvider,
+ useAuthorizedPath,
+ useUserProfile,
+} from './contexts/Config';
import { SessionProvider, useSession } from './contexts/Session';
import AppContainer from './components/AppContainer';
import Background from './components/Background';
@@ -38,6 +42,17 @@ function ErrorFallback({ error }) {
);
}
+const RenderAppContainer = () => {
+ const userProfile = useUserProfile();
+ const navRouteConfig = getRouteConfig(userProfile);
+
+ return (
+
+
+
+ );
+};
+
const AuthorizedRoutes = ({ routeConfig }) => {
const isAuthorized = useAuthorizedPath();
const match = useRouteMatch();
@@ -150,9 +165,7 @@ function App() {
-
-
-
+
diff --git a/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx b/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx
index 80d7c2dc2c..bfd5636b33 100644
--- a/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx
+++ b/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx
@@ -27,7 +27,7 @@ class NavExpandableGroup extends Component {
render() {
const { groupId, groupTitle, routes } = this.props;
- if (routes.length === 1) {
+ if (routes.length === 1 && groupId === 'settings') {
const [{ path }] = routes;
return (
diff --git a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx
index 2f57399dd6..c00721f7a8 100644
--- a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx
+++ b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx
@@ -127,48 +127,44 @@ function PageHeaderToolbar({
]}
/>
- {t`User`}}>
-
-
-
- {loggedInUser && (
-
- {loggedInUser.username}
-
- )}
-
- }
- dropdownItems={[
-
- {t`User Details`}
- ,
-
- {t`Logout`}
- ,
- ]}
- />
-
-
+
+
+
+ {loggedInUser && (
+
+ {loggedInUser.username}
+
+ )}
+
+ }
+ dropdownItems={[
+
+ {t`User Details`}
+ ,
+
+ {t`Logout`}
+ ,
+ ]}
+ />
+
);
diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx
index d624f683d2..18174f8b08 100644
--- a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx
+++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx
@@ -54,6 +54,7 @@ describe('', () => {
test('should fetch and render list items', () => {
expect(fetchRequest).toHaveBeenCalledTimes(1);
+ expect(optionsRequest).toHaveBeenCalledTimes(1);
expect(wrapper.find('CheckboxListItem').length).toBe(3);
});
diff --git a/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx b/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx
index 720595e013..5a123ebe49 100644
--- a/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx
+++ b/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx
@@ -21,6 +21,7 @@ function ExecutionEnvironmentDetail({
isDefaultEnvironment,
virtualEnvironment,
verifyMissingVirtualEnv,
+ helpText,
}) {
const label = isDefaultEnvironment
? t`Default Execution Environment`
@@ -37,6 +38,7 @@ function ExecutionEnvironmentDetail({
{executionEnvironment.name}
}
+ helpText={helpText}
dataCy="execution-environment-detail"
/>
);
@@ -95,6 +97,7 @@ ExecutionEnvironmentDetail.propTypes = {
isDefaultEnvironment: bool,
virtualEnvironment: string,
verifyMissingVirtualEnv: bool,
+ helpText: string,
};
ExecutionEnvironmentDetail.defaultProps = {
@@ -102,6 +105,7 @@ ExecutionEnvironmentDetail.defaultProps = {
executionEnvironment: null,
virtualEnvironment: '',
verifyMissingVirtualEnv: true,
+ helpText: '',
};
export default ExecutionEnvironmentDetail;
diff --git a/awx/ui_next/src/components/HostForm/HostForm.jsx b/awx/ui_next/src/components/HostForm/HostForm.jsx
index a9e1162909..6df19164d5 100644
--- a/awx/ui_next/src/components/HostForm/HostForm.jsx
+++ b/awx/ui_next/src/components/HostForm/HostForm.jsx
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import { bool, func, shape } from 'prop-types';
import { Formik, useField, useFormikContext } from 'formik';
import { t } from '@lingui/macro';
-import { Form, FormGroup } from '@patternfly/react-core';
+import { Form, FormGroup, Tooltip } from '@patternfly/react-core';
import FormField, { FormSubmitError } from '../FormField';
import FormActionGroup from '../FormActionGroup/FormActionGroup';
import { VariablesField } from '../CodeEditor';
@@ -11,7 +11,7 @@ import { FormColumnLayout, FormFullWidthLayout } from '../FormLayout';
import Popover from '../Popover';
import { required } from '../../util/validators';
-const InventoryLookupField = () => {
+const InventoryLookupField = ({ isDisabled }) => {
const { setFieldValue, setFieldTouched } = useFormikContext();
const [inventoryField, inventoryMeta, inventoryHelpers] = useField(
'inventory'
@@ -25,6 +25,23 @@ const InventoryLookupField = () => {
[setFieldValue, setFieldTouched]
);
+ const renderInventoryLookup = (
+ inventoryHelpers.setTouched()}
+ tooltip={t`Select the inventory that this host will belong to.`}
+ isValid={!inventoryMeta.touched || !inventoryMeta.error}
+ helperTextInvalid={inventoryMeta.error}
+ onChange={handleInventoryUpdate}
+ required
+ touched={inventoryMeta.touched}
+ error={inventoryMeta.error}
+ validate={required(t`Select a value for this field`)}
+ isDisabled={isDisabled}
+ />
+ );
+
return (
{
}
helperTextInvalid={inventoryMeta.error}
>
- inventoryHelpers.setTouched()}
- tooltip={t`Select the inventory that this host will belong to.`}
- isValid={!inventoryMeta.touched || !inventoryMeta.error}
- helperTextInvalid={inventoryMeta.error}
- onChange={handleInventoryUpdate}
- required
- touched={inventoryMeta.touched}
- error={inventoryMeta.error}
- validate={required(t`Select a value for this field`)}
- />
+ {isDisabled ? (
+
+ {renderInventoryLookup}
+
+ ) : (
+ renderInventoryLookup
+ )}
);
};
@@ -63,6 +74,7 @@ const HostForm = ({
host,
isInventoryVisible,
submitError,
+ disableInventoryLookup,
}) => {
return (
- {isInventoryVisible && }
+ {isInventoryVisible && (
+
+ )}
', () => {
expect(wrapper.find('input#host-description').prop('value')).toEqual(
'new bar'
);
+ expect(wrapper.find('InventoryLookup').prop('isDisabled')).toEqual(false);
});
test('calls handleSubmit when form submitted', async () => {
@@ -84,4 +85,18 @@ describe('', () => {
});
expect(wrapper.find('InventoryLookupField').length).toBe(0);
});
+
+ test('inventory lookup field should be disabled', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(wrapper.find('InventoryLookup').prop('isDisabled')).toEqual(true);
+ });
});
diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx
index ade6f62c20..85d2ade5bd 100644
--- a/awx/ui_next/src/components/JobList/JobListItem.jsx
+++ b/awx/ui_next/src/components/JobList/JobListItem.jsx
@@ -14,6 +14,7 @@ import ChipGroup from '../ChipGroup';
import CredentialChip from '../CredentialChip';
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
import { formatDateString } from '../../util/dates';
+import { isJobRunning } from '../../util/jobs';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
import JobCancelButton from '../JobCancelButton';
@@ -32,7 +33,7 @@ function JobListItem({
const jobTypes = {
project_update: t`Source Control Update`,
inventory_update: t`Inventory Sync`,
- job: t`Playbook Run`,
+ job: job.job_type === 'check' ? t`Playbook Check` : t`Playbook Run`,
ad_hoc_command: t`Command`,
system_job: t`Management Job`,
workflow_job: t`Workflow Job`,
@@ -202,10 +203,12 @@ function JobListItem({
dataCy={`job-${job.id}-project`}
/>
)}
-
+ {job.type !== 'workflow_job' && !isJobRunning(job.status) && (
+
+ )}
{credentials && credentials.length > 0 && (
@@ -120,7 +121,7 @@ function InventoryLookup({
fieldName={fieldName}
validate={validate}
isLoading={isLoading}
- isDisabled={!canEdit}
+ isDisabled={!canEdit || isDisabled}
qsConfig={QS_CONFIG}
renderOptionsList={({ state, dispatch, canDelete }) => (
(
{},
fieldName: 'inventory',
+ isDisabled: false,
};
export default withRouter(InventoryLookup);
diff --git a/awx/ui_next/src/components/Lookup/Lookup.jsx b/awx/ui_next/src/components/Lookup/Lookup.jsx
index 04842332ad..0be5b61b85 100644
--- a/awx/ui_next/src/components/Lookup/Lookup.jsx
+++ b/awx/ui_next/src/components/Lookup/Lookup.jsx
@@ -215,6 +215,7 @@ Lookup.propTypes = {
fieldName: string.isRequired,
validate: func,
onDebounce: func,
+ isDisabled: bool,
};
Lookup.defaultProps = {
@@ -235,6 +236,7 @@ Lookup.defaultProps = {
),
validate: () => undefined,
onDebounce: () => undefined,
+ isDisabled: false,
};
export { Lookup as _Lookup };
diff --git a/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx
index 0dd41a5066..3780de727a 100644
--- a/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx
+++ b/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx
@@ -1,9 +1,13 @@
import React from 'react';
-
import { t } from '@lingui/macro';
import { Link } from 'react-router-dom';
-
-import { Chip, List, ListItem } from '@patternfly/react-core';
+import {
+ Chip,
+ TextList,
+ TextListItem,
+ TextListVariants,
+ TextListItemVariants,
+} from '@patternfly/react-core';
import { Detail, DeletedDetail } from '../DetailList';
import { VariablesDetail } from '../CodeEditor';
import CredentialChip from '../CredentialChip';
@@ -44,14 +48,28 @@ function PromptInventorySourceDetail({ resource }) {
update_on_project_update
) {
optionsList = (
-
- {overwrite && {t`Overwrite`}}
- {overwrite_vars && {t`Overwrite Variables`}}
- {update_on_launch && {t`Update on Launch`}}
- {update_on_project_update && (
- {t`Update on Project Update`}
+
+ {overwrite && (
+
+ {t`Overwrite local groups and hosts from remote inventory source`}
+
)}
-
+ {overwrite_vars && (
+
+ {t`Overwrite local variables from remote inventory source`}
+
+ )}
+ {update_on_launch && (
+
+ {t`Update on launch`}
+
+ )}
+ {update_on_project_update && (
+
+ {t`Update on project update`}
+
+ )}
+
);
}
@@ -162,7 +180,9 @@ function PromptInventorySourceDetail({ resource }) {
}
/>
)}
- {optionsList && }
+ {optionsList && (
+
+ )}
{source_vars && (
{
);
expect(
wrapper
- .find('Detail[label="Options"]')
+ .find('Detail[label="Enabled Options"]')
.containsAllMatchingElements([
- Overwrite,
- Overwrite Variables,
- Update on Launch,
- Update on Project Update,
+
+ Overwrite local groups and hosts from remote inventory source
+ ,
+ Overwrite local variables from remote inventory source,
+ Update on launch,
+ Update on project update,
])
).toEqual(true);
});
diff --git a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx
index 73fdb3a48c..00b2cf93c9 100644
--- a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx
+++ b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx
@@ -1,9 +1,13 @@
import React from 'react';
-
import { t } from '@lingui/macro';
import { Link } from 'react-router-dom';
-
-import { Chip, List, ListItem } from '@patternfly/react-core';
+import {
+ Chip,
+ TextList,
+ TextListItem,
+ TextListVariants,
+ TextListItemVariants,
+} from '@patternfly/react-core';
import CredentialChip from '../CredentialChip';
import ChipGroup from '../ChipGroup';
import Sparkline from '../Sparkline';
@@ -51,19 +55,37 @@ function PromptJobTemplateDetail({ resource }) {
become_enabled ||
host_config_key ||
allow_simultaneous ||
- use_fact_cache
+ use_fact_cache ||
+ webhook_service
) {
optionsList = (
-
+
{become_enabled && (
- {t`Enable Privilege Escalation`}
+
+ {t`Privilege Escalation`}
+
)}
{host_config_key && (
- {t`Allow Provisioning Callbacks`}
+
+ {t`Provisioning Callbacks`}
+
)}
- {allow_simultaneous && {t`Enable Concurrent Jobs`}}
- {use_fact_cache && {t`Use Fact Storage`}}
-
+ {allow_simultaneous && (
+
+ {t`Concurrent Jobs`}
+
+ )}
+ {use_fact_cache && (
+
+ {t`Fact Storage`}
+
+ )}
+ {webhook_service && (
+
+ {t`Webhooks`}
+
+ )}
+
);
}
@@ -164,7 +186,7 @@ function PromptJobTemplateDetail({ resource }) {
}
/>
)}
- {optionsList && }
+ {optionsList && }
{summary_fields?.credentials?.length > 0 && (
{
).toEqual(true);
expect(
wrapper
- .find('Detail[label="Options"]')
+ .find('Detail[label="Enabled Options"]')
.containsAllMatchingElements([
- Enable Privilege Escalation,
- Allow Provisioning Callbacks,
- Enable Concurrent Jobs,
- Use Fact Storage,
+ Privilege Escalation,
+ Provisioning Callbacks,
+ Concurrent Jobs,
+ Fact Storage,
+ Webhooks,
])
).toEqual(true);
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
diff --git a/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx
index dd2fe8b5a9..a779e026dc 100644
--- a/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx
+++ b/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx
@@ -1,10 +1,13 @@
import React from 'react';
-
import { t } from '@lingui/macro';
-import { List, ListItem } from '@patternfly/react-core';
+import {
+ TextList,
+ TextListItem,
+ TextListVariants,
+ TextListItemVariants,
+} from '@patternfly/react-core';
import { Link } from 'react-router-dom';
import { Config } from '../../contexts/Config';
-
import { Detail, DeletedDetail } from '../DetailList';
import CredentialChip from '../CredentialChip';
import { toTitleCase } from '../../util/strings';
@@ -36,17 +39,33 @@ function PromptProjectDetail({ resource }) {
allow_override
) {
optionsList = (
-
- {scm_clean && {t`Clean`}}
- {scm_delete_on_update && {t`Delete on Update`}}
+
+ {scm_clean && (
+ {t`Discard local changes before syncing`}
+ )}
+ {scm_delete_on_update && (
+ {t`Delete the project before syncing`}
+ )}
{scm_track_submodules && (
- {t`Track submodules latest commit on branch`}
+ {t`Track submodules latest commit on branch`}
)}
{scm_update_on_launch && (
- {t`Update Revision on Launch`}
+ {t`Update revision on job launch`}
)}
- {allow_override && {t`Allow Branch Override`}}
-
+ {allow_override && (
+ {t`Allow branch override`}
+ )}
+
);
}
@@ -90,7 +109,7 @@ function PromptProjectDetail({ resource }) {
}
/>
)}
- {optionsList && }
+ {optionsList && }
{
beforeAll(() => {
wrapper = mountWithContexts(
- ,
+ ,
{
context: { config },
}
@@ -54,12 +56,13 @@ describe('PromptProjectDetail', () => {
);
expect(
wrapper
- .find('Detail[label="Options"]')
+ .find('Detail[label="Enabled Options"]')
.containsAllMatchingElements([
- Clean,
- Delete on Update,
- Update Revision on Launch,
- Allow Branch Override,
+ Discard local changes before syncing,
+ Delete the project before syncing,
+ Track submodules latest commit on branch,
+ Update revision on job launch,
+ Allow branch override,
])
).toEqual(true);
});
diff --git a/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx
index 935c187864..4a54fb31d8 100644
--- a/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx
+++ b/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx
@@ -1,9 +1,13 @@
import React from 'react';
-
import { t } from '@lingui/macro';
import { Link } from 'react-router-dom';
-
-import { Chip, List, ListItem } from '@patternfly/react-core';
+import {
+ Chip,
+ TextList,
+ TextListItem,
+ TextListVariants,
+ TextListItemVariants,
+} from '@patternfly/react-core';
import CredentialChip from '../CredentialChip';
import ChipGroup from '../ChipGroup';
import { Detail } from '../DetailList';
@@ -26,10 +30,18 @@ function PromptWFJobTemplateDetail({ resource }) {
let optionsList = '';
if (allow_simultaneous || webhook_service) {
optionsList = (
-
- {allow_simultaneous && {t`Enable Concurrent Jobs`}}
- {webhook_service && {t`Enable Webhooks`}}
-
+
+ {allow_simultaneous && (
+
+ {t`Concurrent Jobs`}
+
+ )}
+ {webhook_service && (
+
+ {t`Webhooks`}
+
+ )}
+
);
}
@@ -82,7 +94,7 @@ function PromptWFJobTemplateDetail({ resource }) {
value={`${window.location.origin}${related.webhook_receiver}`}
/>
)}
- {optionsList && }
+ {optionsList && }
{summary_fields?.webhook_credential && (
{
);
expect(
wrapper
- .find('Detail[label="Options"]')
+ .find('Detail[label="Enabled Options"]')
.containsAllMatchingElements([
- Enable Concurrent Jobs,
- Enable Webhooks,
+ Concurrent Jobs,
+ Webhooks,
])
).toEqual(true);
expect(
diff --git a/awx/ui_next/src/contexts/Config.jsx b/awx/ui_next/src/contexts/Config.jsx
index e87c2a24f2..5580bd9192 100644
--- a/awx/ui_next/src/contexts/Config.jsx
+++ b/awx/ui_next/src/contexts/Config.jsx
@@ -3,7 +3,7 @@ import { useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro';
-import { ConfigAPI, MeAPI } from '../api';
+import { ConfigAPI, MeAPI, UsersAPI, OrganizationsAPI } from '../api';
import useRequest, { useDismissableError } from '../util/useRequest';
import AlertModal from '../components/AlertModal';
import ErrorDetail from '../components/ErrorDetail';
@@ -35,9 +35,32 @@ export const ConfigProvider = ({ children }) => {
},
},
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
- return { ...data, me };
+
+ const [
+ {
+ data: { count: adminOrgCount },
+ },
+ {
+ data: { count: notifAdminCount },
+ },
+ {
+ data: { count: execEnvAdminCount },
+ },
+ ] = await Promise.all([
+ UsersAPI.readAdminOfOrganizations(me?.id),
+ OrganizationsAPI.read({
+ page_size: 1,
+ role_level: 'notification_admin_role',
+ }),
+ OrganizationsAPI.read({
+ page_size: 1,
+ role_level: 'execution_environment_admin_role',
+ }),
+ ]);
+
+ return { ...data, me, adminOrgCount, notifAdminCount, execEnvAdminCount };
}, []),
- {}
+ { adminOrgCount: 0, notifAdminCount: 0, execEnvAdminCount: 0 }
);
const { error, dismissError } = useDismissableError(configError);
@@ -77,6 +100,17 @@ export const ConfigProvider = ({ children }) => {
);
};
+export const useUserProfile = () => {
+ const config = useConfig();
+ return {
+ isSuperUser: !!config.me?.is_superuser,
+ isSystemAuditor: !!config.me?.is_system_auditor,
+ isOrgAdmin: config.adminOrgCount,
+ isNotificationAdmin: config.notifAdminCount,
+ isExecEnvAdmin: config.execEnvAdminCount,
+ };
+};
+
export const useAuthorizedPath = () => {
const config = useConfig();
const subscriptionMgmtRoute = useRouteMatch({
diff --git a/awx/ui_next/src/routeConfig.jsx b/awx/ui_next/src/routeConfig.jsx
index fba640a2d8..5d29072fcd 100644
--- a/awx/ui_next/src/routeConfig.jsx
+++ b/awx/ui_next/src/routeConfig.jsx
@@ -22,8 +22,8 @@ import Users from './screens/User';
import WorkflowApprovals from './screens/WorkflowApproval';
import { Jobs } from './screens/Job';
-function getRouteConfig() {
- return [
+function getRouteConfig(userProfile = {}) {
+ let routeConfig = [
{
groupTitle: Views,
groupId: 'views_group',
@@ -155,6 +155,29 @@ function getRouteConfig() {
],
},
];
+
+ const deleteRoute = name => {
+ routeConfig.forEach(group => {
+ group.routes = group.routes.filter(({ path }) => !path.includes(name));
+ });
+ routeConfig = routeConfig.filter(groups => groups.routes.length);
+ };
+
+ const deleteRouteGroup = name => {
+ routeConfig = routeConfig.filter(({ groupId }) => !groupId.includes(name));
+ };
+
+ if (userProfile?.isSuperUser || userProfile?.isSystemAuditor)
+ return routeConfig;
+ deleteRouteGroup('settings');
+ deleteRoute('management_jobs');
+ deleteRoute('credential_types');
+ if (userProfile?.isOrgAdmin) return routeConfig;
+ deleteRoute('applications');
+ deleteRoute('instance_groups');
+ if (!userProfile?.isNotificationAdmin) deleteRoute('notification_templates');
+
+ return routeConfig;
}
export default getRouteConfig;
diff --git a/awx/ui_next/src/routeConfig.test.jsx b/awx/ui_next/src/routeConfig.test.jsx
new file mode 100644
index 0000000000..b4382bbcd0
--- /dev/null
+++ b/awx/ui_next/src/routeConfig.test.jsx
@@ -0,0 +1,248 @@
+import getRouteConfig from './routeConfig';
+
+const userProfile = {
+ isSuperUser: false,
+ isSystemAuditor: false,
+ isOrgAdmin: false,
+ isNotificationAdmin: false,
+ isExecEnvAdmin: false,
+};
+
+const filterPaths = sidebar => {
+ const visibleRoutes = [];
+ sidebar.forEach(({ routes }) => {
+ routes.forEach(route => {
+ visibleRoutes.push(route.path);
+ });
+ });
+
+ return visibleRoutes;
+};
+describe('getRouteConfig', () => {
+ test('routes for system admin', () => {
+ const sidebar = getRouteConfig({ ...userProfile, isSuperUser: true });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/credential_types',
+ '/notification_templates',
+ '/management_jobs',
+ '/instance_groups',
+ '/applications',
+ '/execution_environments',
+ '/settings',
+ ]);
+ });
+
+ test('routes for system auditor', () => {
+ const sidebar = getRouteConfig({ ...userProfile, isSystemAuditor: true });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/credential_types',
+ '/notification_templates',
+ '/management_jobs',
+ '/instance_groups',
+ '/applications',
+ '/execution_environments',
+ '/settings',
+ ]);
+ });
+
+ test('routes for org admin', () => {
+ const sidebar = getRouteConfig({ ...userProfile, isOrgAdmin: true });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/notification_templates',
+ '/instance_groups',
+ '/applications',
+ '/execution_environments',
+ ]);
+ });
+
+ test('routes for notifications admin', () => {
+ const sidebar = getRouteConfig({
+ ...userProfile,
+ isNotificationAdmin: true,
+ });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/notification_templates',
+ '/execution_environments',
+ ]);
+ });
+
+ test('routes for execution environments admin', () => {
+ const sidebar = getRouteConfig({ ...userProfile, isExecEnvAdmin: true });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/execution_environments',
+ ]);
+ });
+
+ test('routes for regular users', () => {
+ const sidebar = getRouteConfig(userProfile);
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/execution_environments',
+ ]);
+ });
+
+ test('routes for execution environment admins and notification admin', () => {
+ const sidebar = getRouteConfig({
+ ...userProfile,
+ isExecEnvAdmin: true,
+ isNotificationAdmin: true,
+ });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/notification_templates',
+ '/execution_environments',
+ ]);
+ });
+
+ test('routes for execution environment admins and organization admins', () => {
+ const sidebar = getRouteConfig({
+ ...userProfile,
+ isExecEnvAdmin: true,
+ isOrgAdmin: true,
+ });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/notification_templates',
+ '/instance_groups',
+ '/applications',
+ '/execution_environments',
+ ]);
+ });
+
+ test('routes for notification admins and organization admins', () => {
+ const sidebar = getRouteConfig({
+ ...userProfile,
+ isNotificationAdmin: true,
+ isOrgAdmin: true,
+ });
+ const filteredPaths = filterPaths(sidebar);
+ expect(filteredPaths).toEqual([
+ '/home',
+ '/jobs',
+ '/schedules',
+ '/activity_stream',
+ '/workflow_approvals',
+ '/templates',
+ '/credentials',
+ '/projects',
+ '/inventories',
+ '/hosts',
+ '/organizations',
+ '/users',
+ '/teams',
+ '/notification_templates',
+ '/instance_groups',
+ '/applications',
+ '/execution_environments',
+ ]);
+ });
+});
diff --git a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
index 9ca1c1ed0d..0d32e6ec44 100644
--- a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
@@ -1,9 +1,14 @@
import React, { Fragment, useEffect, useCallback } from 'react';
import { Link, useHistory } from 'react-router-dom';
-
import { t } from '@lingui/macro';
import styled from 'styled-components';
-import { Button, List, ListItem } from '@patternfly/react-core';
+import {
+ Button,
+ TextList,
+ TextListItem,
+ TextListVariants,
+ TextListItemVariants,
+} from '@patternfly/react-core';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
import ContentError from '../../../components/ContentError';
@@ -134,15 +139,7 @@ function CredentialDetail({ credential }) {
}
if (type === 'boolean') {
- return (
- {inputs[id] && {label}}}
- />
- );
+ return null;
}
if (inputs[id] === '$encrypted$') {
@@ -189,6 +186,10 @@ function CredentialDetail({ credential }) {
credential
);
+ const enabledBooleanFields = fields.filter(
+ ({ id, type }) => type === 'boolean' && inputs[id]
+ );
+
if (hasContentLoading) {
return ;
}
@@ -255,6 +256,20 @@ function CredentialDetail({ credential }) {
date={modified}
user={modified_by}
/>
+ {enabledBooleanFields.length > 0 && (
+
+ {enabledBooleanFields.map(({ id, label }) => (
+
+ {label}
+
+ ))}
+
+ }
+ />
+ )}
{Object.keys(inputSources).length > 0 && (
diff --git a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx
index 25147bb148..14e48a3569 100644
--- a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx
@@ -111,7 +111,7 @@ describe('', () => {
'Privilege Escalation Password',
'Prompt on launch'
);
- expect(wrapper.find(`Detail[label="Options"] ListItem`).text()).toEqual(
+ expect(wrapper.find(`Detail[label="Enabled Options"] li`).text()).toEqual(
'Authorize'
);
});
diff --git a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx
index 8e90b6d535..f5680562f7 100644
--- a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx
+++ b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx
@@ -34,6 +34,7 @@ function HostEdit({ host }) {
handleSubmit={handleSubmit}
handleCancel={handleCancel}
submitError={formError}
+ disableInventoryLookup
/>
);
diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.jsx
index e66a13457e..6caa945f25 100644
--- a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.jsx
+++ b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.jsx
@@ -141,8 +141,10 @@ function InstanceList() {
[instanceGroupId]
);
- const readInstancesOptions = () =>
- InstanceGroupsAPI.readInstanceOptions(instanceGroupId);
+ const readInstancesOptions = useCallback(
+ () => InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
+ [instanceGroupId]
+ );
return (
<>
diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
index 5f9213328b..bc617d7a68 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
@@ -1,9 +1,13 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
-
import { t } from '@lingui/macro';
-
-import { Button, List, ListItem } from '@patternfly/react-core';
+import {
+ Button,
+ TextList,
+ TextListItem,
+ TextListVariants,
+ TextListItemVariants,
+} from '@patternfly/react-core';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
import { VariablesDetail } from '../../../components/CodeEditor';
@@ -19,7 +23,6 @@ import {
UserDateDetail,
} from '../../../components/DetailList';
import ErrorDetail from '../../../components/ErrorDetail';
-import Popover from '../../../components/Popover';
import useRequest from '../../../util/useRequest';
import { InventorySourcesAPI } from '../../../api';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
@@ -112,71 +115,28 @@ function InventorySourceDetail({ inventorySource }) {
update_on_project_update
) {
optionsList = (
-
+
{overwrite && (
-
- {t`Overwrite`}
-
- {t`If checked, any hosts and groups that were
- previously present on the external source but are now removed
- will be removed from the inventory. Hosts and groups
- that were not managed by the inventory source will be promoted
- to the next manually created group or if there is no manually
- created group to promote them into, they will be left in the "all"
- default group for the inventory.`}
-
-
- {t`When not checked, local child
- hosts and groups not found on the external source will remain
- untouched by the inventory update process.`}
- >
- }
- />
-
+
+ {t`Overwrite local groups and hosts from remote inventory source`}
+
)}
{overwrite_vars && (
-
- {t`Overwrite variables`}
-
- {t`If checked, all variables for child groups
- and hosts will be removed and replaced by those found
- on the external source.`}
-
-
- {t`When not checked, a merge will be performed,
- combining local variables with those found on the
- external source.`}
- >
- }
- />
-
+
+ {t`Overwrite local variables from remote inventory source`}
+
)}
{update_on_launch && (
-
+
{t`Update on launch`}
-
-
+
)}
{update_on_project_update && (
-
+
{t`Update on project update`}
-
-
+
)}
-
+
);
}
@@ -242,7 +202,7 @@ function InventorySourceDetail({ inventorySource }) {
/>
)}
{optionsList && (
-
+
)}
{source_vars && (
{
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
'---\nfoo: bar'
);
- wrapper.find('Detail[label="Options"] li').forEach(option => {
+ wrapper.find('Detail[label="Enabled Options"] li').forEach(option => {
expect([
- 'Overwrite',
- 'Overwrite variables',
+ 'Overwrite local groups and hosts from remote inventory source',
+ 'Overwrite local variables from remote inventory source',
'Update on launch',
'Update on project update',
]).toContain(option.text());
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
index d490652138..b198e744d4 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
@@ -28,25 +28,22 @@ function SmartInventory({ setBreadcrumb }) {
const match = useRouteMatch('/inventories/smart_inventory/:id');
const {
- result: { inventory },
+ result: inventory,
error: contentError,
isLoading: hasContentLoading,
request: fetchInventory,
} = useRequest(
useCallback(async () => {
const { data } = await InventoriesAPI.readDetail(match.params.id);
- return {
- inventory: data,
- };
+ return data;
}, [match.params.id]),
- {
- inventory: null,
- }
+
+ null
);
useEffect(() => {
fetchInventory();
- }, [fetchInventory]);
+ }, [fetchInventory, location.pathname]);
useEffect(() => {
if (inventory) {
diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
index 7b8ab75fe5..2b272dd13c 100644
--- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
+++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
@@ -79,7 +79,7 @@ function JobDetail({ job }) {
inventory_update: t`Inventory Sync`,
job: job.job_type === 'check' ? t`Playbook Check` : t`Playbook Run`,
ad_hoc_command: t`Command`,
- management_job: t`Management Job`,
+ system_job: t`Management Job`,
workflow_job: t`Workflow Job`,
};
@@ -220,10 +220,12 @@ function JobDetail({ job }) {
-
+ {job.type !== 'workflow_job' && !isJobRunning(job.status) && (
+
+ )}
{instanceGroup && !instanceGroup?.is_container_group && (
n + !node.isDeleted, 0) - 1;
-
+ const navToWorkflow = () => {
+ history.push(
+ `/templates/workflow_job_template/${job.unified_job_template}/visualizer`
+ );
+ };
return (
@@ -70,6 +79,15 @@ function WorkflowOutputToolbar({ job }) {
{job.name}
+
+
+
{t`Total Nodes`}
{totalNodes}
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx
index 6fbe4ff9eb..b597c35f0d 100644
--- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx
@@ -18,6 +18,9 @@ const workflowContext = {
showTools: false,
};
+function shouldFind(element) {
+ expect(wrapper.find(element)).toHaveLength(1);
+}
describe('WorkflowOutputToolbar', () => {
beforeAll(() => {
const nodes = [
@@ -45,6 +48,13 @@ describe('WorkflowOutputToolbar', () => {
wrapper.unmount();
});
+ test('should render correct toolbar item', () => {
+ shouldFind(`Button[ouiaId="edit-workflow"]`);
+ shouldFind('Button#workflow-output-toggle-legend');
+ shouldFind('Badge');
+ shouldFind('Button#workflow-output-toggle-tools');
+ });
+
test('Shows correct number of nodes', () => {
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
expect(wrapper.find('Badge').text()).toBe('1');
diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx
index 647b441075..a2f62e851c 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx
@@ -133,11 +133,13 @@ function OrganizationDetail({ organization }) {
value={
{galaxy_credentials.map(credential => (
-
+
+
+
))}
}
diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
index 6e1bad5b24..0882456b5f 100644
--- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
@@ -5,8 +5,10 @@ import styled from 'styled-components';
import {
Button,
ClipboardCopy,
- List,
- ListItem,
+ TextList,
+ TextListItem,
+ TextListVariants,
+ TextListItemVariants,
Tooltip,
} from '@patternfly/react-core';
import { Project } from '../../../types';
@@ -78,17 +80,33 @@ function ProjectDetail({ project }) {
allow_override
) {
optionsList = (
-
- {scm_clean && {t`Clean`}}
- {scm_delete_on_update && {t`Delete on Update`}}
+
+ {scm_clean && (
+ {t`Discard local changes before syncing`}
+ )}
+ {scm_delete_on_update && (
+ {t`Delete the project before syncing`}
+ )}
{scm_track_submodules && (
- {t`Track submodules latest commit on branch`}
+ {t`Track submodules latest commit on branch`}
)}
{scm_update_on_launch && (
- {t`Update Revision on Launch`}
+ {t`Update revision on job launch`}
)}
- {allow_override && {t`Allow Branch Override`}}
-
+ {allow_override && (
+ {t`Allow branch override`}
+ )}
+
);
}
@@ -196,7 +214,6 @@ function ProjectDetail({ project }) {
}
/>
)}
- {optionsList && }
-
+ {optionsList && (
+
+ )}
{summary_fields.user_capabilities?.edit && (
diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
index 34ef0d6beb..fa627ff09f 100644
--- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
@@ -70,7 +70,7 @@ describe('', () => {
scm_refspec: 'refs/remotes/*',
scm_clean: true,
scm_delete_on_update: true,
- scm_track_submodules: false,
+ scm_track_submodules: true,
credential: 100,
status: 'successful',
organization: 10,
@@ -127,12 +127,13 @@ describe('', () => {
);
expect(
wrapper
- .find('Detail[label="Options"]')
+ .find('Detail[label="Enabled Options"]')
.containsAllMatchingElements([
- Clean,
- Delete on Update,
- Update Revision on Launch,
- Allow Branch Override,
+ Discard local changes before syncing,
+ Delete the project before syncing,
+ Track submodules latest commit on branch,
+ Update revision on job launch,
+ Allow branch override,
])
).toEqual(true);
});
@@ -151,7 +152,7 @@ describe('', () => {
const wrapper = mountWithContexts(
);
- expect(wrapper.find('Detail[label="Options"]').length).toBe(0);
+ expect(wrapper.find('Detail[label="Enabled Options"]').length).toBe(0);
});
test('should have proper number of delete detail requests', () => {
diff --git a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.jsx b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.jsx
index 3508f32138..a8fed74240 100644
--- a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.jsx
+++ b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.jsx
@@ -23,7 +23,7 @@ import {
} from '../../../../util/dates';
function SubscriptionDetail() {
- const { license_info, version } = useConfig();
+ const { me = {}, license_info, version } = useConfig();
const baseURL = '/settings/subscription';
const tabsArray = [
{
@@ -164,15 +164,17 @@ function SubscriptionDetail() {
contact us.
-
-
-
+ {me.is_superuser && (
+
+
+
+ )}
>
);
diff --git a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.jsx b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.jsx
index 3b79c35db8..fffea57a4f 100644
--- a/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.jsx
+++ b/awx/ui_next/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.jsx
@@ -71,6 +71,14 @@ describe('', () => {
assertDetail('Hosts remaining', '1000');
assertDetail('Hosts automated', '12 since 3/2/2021, 7:43:48 PM');
+ expect(wrapper.find('Button[aria-label="edit"]').length).toBe(0);
+ });
+
+ test('should render edit button for system admin', () => {
+ wrapper = mountWithContexts(, {
+ context: { ...config, me: { is_superuser: true } },
+ });
+
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1);
});
});
diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
index 3e13e58ba9..6fcc51714e 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
@@ -115,28 +115,37 @@ function JobTemplateDetail({ template }) {
);
const generateCallBackUrl = `${window.location.origin + url}callback/`;
const renderOptionsField =
- become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
+ become_enabled ||
+ host_config_key ||
+ allow_simultaneous ||
+ use_fact_cache ||
+ webhook_service;
const renderOptions = (
{become_enabled && (
- {t`Enable Privilege Escalation`}
+ {t`Privilege Escalation`}
)}
{host_config_key && (
- {t`Allow Provisioning Callbacks`}
+ {t`Provisioning Callbacks`}
)}
{allow_simultaneous && (
- {t`Enable Concurrent Jobs`}
+ {t`Concurrent Jobs`}
)}
{use_fact_cache && (
- {t`Use Fact Storage`}
+ {t`Fact Storage`}
+
+ )}
+ {webhook_service && (
+
+ {t`Webhooks`}
)}
@@ -166,7 +175,6 @@ function JobTemplateDetail({ template }) {
if (isLoadingInstanceGroups || isDeleteLoading) {
return ;
}
-
return (
@@ -212,7 +220,10 @@ function JobTemplateDetail({ template }) {
)}
@@ -256,9 +267,6 @@ function JobTemplateDetail({ template }) {
}
/>
)}
- {renderOptionsField && (
-
- )}
+ {renderOptionsField && (
+
+ )}
{summary_fields.credentials && summary_fields.credentials.length > 0 && (
{summary_fields.credentials.map(c => (
-
+
+
+
))}
}
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
index f3b02c812b..89e17ea577 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
@@ -58,12 +58,12 @@ function WorkflowJobTemplateDetail({ template }) {
{template.allow_simultaneous && (
- {t`- Enable Concurrent Jobs`}
+ {t`Concurrent Jobs`}
)}
{template.webhook_service && (
- {t`- Enable Webhooks`}
+ {t`Webhooks`}
)}
@@ -186,9 +186,6 @@ function WorkflowJobTemplateDetail({ template }) {
}
/>
)}
- {renderOptionsField && (
-
- )}
+ {renderOptionsField && (
+
+ )}
{summary_fields.labels?.results?.length > 0 && (
0;
const externalAccount = user.external_account;
@@ -119,22 +120,24 @@ function UserFormFields({ user }) {
validate={required(t`Select a value for this field`)}
/>
)}
-
-
-
+ {me.is_superuser && (
+
+
+
+ )}
>
);
}
diff --git a/awx/ui_next/src/screens/User/shared/UserForm.test.jsx b/awx/ui_next/src/screens/User/shared/UserForm.test.jsx
index d2bb6e3764..69c777bc5b 100644
--- a/awx/ui_next/src/screens/User/shared/UserForm.test.jsx
+++ b/awx/ui_next/src/screens/User/shared/UserForm.test.jsx
@@ -230,4 +230,25 @@ describe('', () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
expect(handleCancel).toBeCalled();
});
+
+ test('should not show user type field', async () => {
+ const handleCancel = jest.fn();
+ await act(async () => {
+ wrapper = mountWithContexts(
+ ,
+ {
+ context: {
+ config: {
+ me: { is_superuser: false },
+ },
+ },
+ }
+ );
+ });
+ expect(wrapper.find('FormGroup[label="User Type"]')).toHaveLength(0);
+ });
});
diff --git a/awx_collection/plugins/modules/workflow_job_template.py b/awx_collection/plugins/modules/workflow_job_template.py
index 8a4c5d75ec..7fc6b66cca 100644
--- a/awx_collection/plugins/modules/workflow_job_template.py
+++ b/awx_collection/plugins/modules/workflow_job_template.py
@@ -47,10 +47,6 @@ options:
description:
- Variables which will be made available to jobs ran inside the workflow.
type: dict
- execution_environment:
- description:
- - Execution Environment to use for the WFJT.
- type: str
organization:
description:
- Organization the workflow job template exists in.
@@ -666,7 +662,6 @@ def main():
description=dict(),
extra_vars=dict(type='dict'),
organization=dict(),
- execution_environment=dict(),
survey_spec=dict(type='dict', aliases=['survey']),
survey_enabled=dict(type='bool'),
allow_simultaneous=dict(type='bool'),
@@ -713,10 +708,6 @@ def main():
organization_id = module.resolve_name_to_id('organizations', organization)
search_fields['organization'] = new_fields['organization'] = organization_id
- ee = module.params.get('execution_environment')
- if ee:
- new_fields['execution_environment'] = module.resolve_name_to_id('execution_environments', ee)
-
# Attempt to look up an existing item based on the provided data
existing_item = module.get_one('workflow_job_templates', name_or_id=name, **{'data': search_fields})
diff --git a/awxkit/awxkit/api/mixins/has_status.py b/awxkit/awxkit/api/mixins/has_status.py
index e4bd603327..057b349672 100644
--- a/awxkit/awxkit/api/mixins/has_status.py
+++ b/awxkit/awxkit/api/mixins/has_status.py
@@ -67,6 +67,14 @@ class HasStatus(object):
msg += '{0}-{1} has status of {2}, which is not in {3}.'.format(self.type.title(), self.id, self.status, status_list)
if getattr(self, 'job_explanation', ''):
msg += '\njob_explanation: {}'.format(bytes_to_str(self.job_explanation))
+ if getattr(self, 'execution_environment', ''):
+ msg += '\nexecution_environment: {}'.format(bytes_to_str(self.execution_environment))
+ if getattr(self, 'related', False):
+ ee = self.related.execution_environment.get()
+ msg += f'\nee_image: {ee.image}'
+ msg += f'\nee_credential: {ee.credential}'
+ msg += f'\nee_pull_option: {ee.pull}'
+ msg += f'\nee_summary_fields: {ee.summary_fields}'
if getattr(self, 'result_traceback', ''):
msg += '\nresult_traceback:\n{}'.format(bytes_to_str(self.result_traceback))
diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt
index a1bae58598..6811a16fd3 100644
--- a/requirements/requirements_dev.txt
+++ b/requirements/requirements_dev.txt
@@ -21,3 +21,5 @@ sdb
remote-pdb
gprof2dot
atomicwrites==1.4.0
+flake8
+yamllint