mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 13:39:27 -02:30
Merge pull request #10465 from shanemcd/downstream-fixes
Downstream fixes Reviewed-by: Alan Rominger <arominge@redhat.com> Reviewed-by: Alex Corey <Alex.swansboro@gmail.com> Reviewed-by: Jake McDermott <yo@jakemcdermott.me>
This commit is contained in:
5
Makefile
5
Makefile
@@ -288,6 +288,11 @@ swagger: reports
|
|||||||
|
|
||||||
check: black
|
check: black
|
||||||
|
|
||||||
|
api-lint:
|
||||||
|
BLACK_ARGS="--check" make black
|
||||||
|
flake8 awx
|
||||||
|
yamllint -s .
|
||||||
|
|
||||||
awx-link:
|
awx-link:
|
||||||
[ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/setup.py egg_info_dev
|
[ -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
|
cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/python$(PYTHON_VERSION)/site-packages/awx.egg-link
|
||||||
|
|||||||
@@ -729,11 +729,12 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
if self.is_detail_view:
|
if self.is_detail_view:
|
||||||
resolved_ee = obj.resolve_execution_environment()
|
resolved_ee = obj.resolve_execution_environment()
|
||||||
summary_fields['resolved_environment'] = {
|
if resolved_ee is not None:
|
||||||
field: getattr(resolved_ee, field, None)
|
summary_fields['resolved_environment'] = {
|
||||||
for field in SUMMARIZABLE_FK_FIELDS['execution_environment']
|
field: getattr(resolved_ee, field, None)
|
||||||
if getattr(resolved_ee, field, None) is not None
|
for field in SUMMARIZABLE_FK_FIELDS['execution_environment']
|
||||||
}
|
if getattr(resolved_ee, field, None) is not None
|
||||||
|
}
|
||||||
|
|
||||||
return summary_fields
|
return summary_fields
|
||||||
|
|
||||||
@@ -3425,6 +3426,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
|
|||||||
'ask_limit_on_launch',
|
'ask_limit_on_launch',
|
||||||
'webhook_service',
|
'webhook_service',
|
||||||
'webhook_credential',
|
'webhook_credential',
|
||||||
|
'-execution_environment',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_related(self, obj):
|
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}),
|
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}),
|
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:
|
if obj.organization:
|
||||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||||
if obj.webhook_credential_id:
|
if obj.webhook_credential_id:
|
||||||
@@ -3502,6 +3505,7 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
|
|||||||
'allow_simultaneous',
|
'allow_simultaneous',
|
||||||
'job_template',
|
'job_template',
|
||||||
'is_sliced_job',
|
'is_sliced_job',
|
||||||
|
'-execution_environment',
|
||||||
'-execution_node',
|
'-execution_node',
|
||||||
'-event_processing_finished',
|
'-event_processing_finished',
|
||||||
'-controller_node',
|
'-controller_node',
|
||||||
@@ -3515,6 +3519,7 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
|
|||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(WorkflowJobSerializer, self).get_related(obj)
|
res = super(WorkflowJobSerializer, self).get_related(obj)
|
||||||
|
res.pop('execution_environment', None) # EEs aren't meaningful for workflows
|
||||||
if obj.workflow_job_template:
|
if obj.workflow_job_template:
|
||||||
res['workflow_job_template'] = self.reverse('api:workflow_job_template_detail', kwargs={'pk': obj.workflow_job_template.pk})
|
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})
|
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 WorkflowJobListSerializer(WorkflowJobSerializer, UnifiedJobListSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('*', '-execution_node', '-controller_node')
|
fields = ('*', '-execution_environment', '-execution_node', '-controller_node')
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobCancelSerializer(WorkflowJobSerializer):
|
class WorkflowJobCancelSerializer(WorkflowJobSerializer):
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class TimingMiddleware(threading.local, MiddlewareMixin):
|
|||||||
response['X-API-Total-Time'] = '%0.3fs' % total_time
|
response['X-API-Total-Time'] = '%0.3fs' % total_time
|
||||||
if settings.AWX_REQUEST_PROFILE:
|
if settings.AWX_REQUEST_PROFILE:
|
||||||
response['X-API-Profile-File'] = self.prof.stop()
|
response['X-API-Profile-File'] = self.prof.stop()
|
||||||
perf_logger.info(
|
perf_logger.debug(
|
||||||
f'request: {request}, response_time: {response["X-API-Total-Time"]}',
|
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"])),
|
extra=dict(python_objects=dict(request=request, response=response, X_API_TOTAL_TIME=response["X-API-Total-Time"])),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1225,6 +1225,10 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
|
|||||||
def is_container_group_task(self):
|
def is_container_group_task(self):
|
||||||
return bool(self.instance_group and self.instance_group.is_container_group)
|
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):
|
def _get_parent_field_name(self):
|
||||||
return 'inventory_source'
|
return 'inventory_source'
|
||||||
|
|
||||||
|
|||||||
@@ -471,13 +471,6 @@ class ExecutionEnvironmentMixin(models.Model):
|
|||||||
template = getattr(self, 'unified_job_template', None)
|
template = getattr(self, 'unified_job_template', None)
|
||||||
if template is not None and template.execution_environment is not None:
|
if template is not None and template.execution_environment is not None:
|
||||||
return template.execution_environment
|
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:
|
if getattr(self, 'project_id', None) and self.project.default_environment is not None:
|
||||||
return self.project.default_environment
|
return self.project.default_environment
|
||||||
if getattr(self, 'organization_id', None) and self.organization.default_environment is not None:
|
if getattr(self, 'organization_id', None) and self.organization.default_environment is not None:
|
||||||
|
|||||||
@@ -595,6 +595,9 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
|||||||
def _get_related_jobs(self):
|
def _get_related_jobs(self):
|
||||||
return WorkflowJob.objects.filter(workflow_job_template=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 WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, WebhookMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import { ErrorBoundary } from 'react-error-boundary';
|
|||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
import { Card, PageSection } from '@patternfly/react-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 { SessionProvider, useSession } from './contexts/Session';
|
||||||
import AppContainer from './components/AppContainer';
|
import AppContainer from './components/AppContainer';
|
||||||
import Background from './components/Background';
|
import Background from './components/Background';
|
||||||
@@ -38,6 +42,17 @@ function ErrorFallback({ error }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RenderAppContainer = () => {
|
||||||
|
const userProfile = useUserProfile();
|
||||||
|
const navRouteConfig = getRouteConfig(userProfile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContainer navRouteConfig={navRouteConfig}>
|
||||||
|
<AuthorizedRoutes routeConfig={navRouteConfig} />
|
||||||
|
</AppContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const AuthorizedRoutes = ({ routeConfig }) => {
|
const AuthorizedRoutes = ({ routeConfig }) => {
|
||||||
const isAuthorized = useAuthorizedPath();
|
const isAuthorized = useAuthorizedPath();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
@@ -150,9 +165,7 @@ function App() {
|
|||||||
</Route>
|
</Route>
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<AppContainer navRouteConfig={getRouteConfig()}>
|
<RenderAppContainer />
|
||||||
<AuthorizedRoutes routeConfig={getRouteConfig()} />
|
|
||||||
</AppContainer>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class NavExpandableGroup extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { groupId, groupTitle, routes } = this.props;
|
const { groupId, groupTitle, routes } = this.props;
|
||||||
|
|
||||||
if (routes.length === 1) {
|
if (routes.length === 1 && groupId === 'settings') {
|
||||||
const [{ path }] = routes;
|
const [{ path }] = routes;
|
||||||
return (
|
return (
|
||||||
<NavItem itemId={groupId} isActive={this.isActivePath(path)} key={path}>
|
<NavItem itemId={groupId} isActive={this.isActivePath(path)} key={path}>
|
||||||
|
|||||||
@@ -127,48 +127,44 @@ function PageHeaderToolbar({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</PageHeaderToolsItem>
|
</PageHeaderToolsItem>
|
||||||
<Tooltip position="left" content={<div>{t`User`}</div>}>
|
<PageHeaderToolsItem>
|
||||||
<PageHeaderToolsItem>
|
<Dropdown
|
||||||
<Dropdown
|
id="toolbar-user-dropdown"
|
||||||
id="toolbar-user-dropdown"
|
isPlain
|
||||||
isPlain
|
isOpen={isUserOpen}
|
||||||
isOpen={isUserOpen}
|
position={DropdownPosition.right}
|
||||||
position={DropdownPosition.right}
|
onSelect={handleUserSelect}
|
||||||
onSelect={handleUserSelect}
|
toggle={
|
||||||
toggle={
|
<DropdownToggle onToggle={setIsUserOpen}>
|
||||||
<DropdownToggle onToggle={setIsUserOpen}>
|
<UserIcon />
|
||||||
<UserIcon />
|
{loggedInUser && (
|
||||||
{loggedInUser && (
|
<span style={{ marginLeft: '10px' }}>
|
||||||
<span style={{ marginLeft: '10px' }}>
|
{loggedInUser.username}
|
||||||
{loggedInUser.username}
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
</DropdownToggle>
|
||||||
</DropdownToggle>
|
}
|
||||||
}
|
dropdownItems={[
|
||||||
dropdownItems={[
|
<DropdownItem
|
||||||
<DropdownItem
|
key="user"
|
||||||
key="user"
|
aria-label={t`User details`}
|
||||||
aria-label={t`User details`}
|
href={
|
||||||
href={
|
loggedInUser ? `#/users/${loggedInUser.id}/details` : '#/home'
|
||||||
loggedInUser
|
}
|
||||||
? `#/users/${loggedInUser.id}/details`
|
>
|
||||||
: '#/home'
|
{t`User Details`}
|
||||||
}
|
</DropdownItem>,
|
||||||
>
|
<DropdownItem
|
||||||
{t`User Details`}
|
key="logout"
|
||||||
</DropdownItem>,
|
component="button"
|
||||||
<DropdownItem
|
onClick={onLogoutClick}
|
||||||
key="logout"
|
id="logout-button"
|
||||||
component="button"
|
>
|
||||||
onClick={onLogoutClick}
|
{t`Logout`}
|
||||||
id="logout-button"
|
</DropdownItem>,
|
||||||
>
|
]}
|
||||||
{t`Logout`}
|
/>
|
||||||
</DropdownItem>,
|
</PageHeaderToolsItem>
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</PageHeaderToolsItem>
|
|
||||||
</Tooltip>
|
|
||||||
</PageHeaderToolsGroup>
|
</PageHeaderToolsGroup>
|
||||||
</PageHeaderTools>
|
</PageHeaderTools>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ describe('<AssociateModal />', () => {
|
|||||||
|
|
||||||
test('should fetch and render list items', () => {
|
test('should fetch and render list items', () => {
|
||||||
expect(fetchRequest).toHaveBeenCalledTimes(1);
|
expect(fetchRequest).toHaveBeenCalledTimes(1);
|
||||||
|
expect(optionsRequest).toHaveBeenCalledTimes(1);
|
||||||
expect(wrapper.find('CheckboxListItem').length).toBe(3);
|
expect(wrapper.find('CheckboxListItem').length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function ExecutionEnvironmentDetail({
|
|||||||
isDefaultEnvironment,
|
isDefaultEnvironment,
|
||||||
virtualEnvironment,
|
virtualEnvironment,
|
||||||
verifyMissingVirtualEnv,
|
verifyMissingVirtualEnv,
|
||||||
|
helpText,
|
||||||
}) {
|
}) {
|
||||||
const label = isDefaultEnvironment
|
const label = isDefaultEnvironment
|
||||||
? t`Default Execution Environment`
|
? t`Default Execution Environment`
|
||||||
@@ -37,6 +38,7 @@ function ExecutionEnvironmentDetail({
|
|||||||
{executionEnvironment.name}
|
{executionEnvironment.name}
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
helpText={helpText}
|
||||||
dataCy="execution-environment-detail"
|
dataCy="execution-environment-detail"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -95,6 +97,7 @@ ExecutionEnvironmentDetail.propTypes = {
|
|||||||
isDefaultEnvironment: bool,
|
isDefaultEnvironment: bool,
|
||||||
virtualEnvironment: string,
|
virtualEnvironment: string,
|
||||||
verifyMissingVirtualEnv: bool,
|
verifyMissingVirtualEnv: bool,
|
||||||
|
helpText: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
ExecutionEnvironmentDetail.defaultProps = {
|
ExecutionEnvironmentDetail.defaultProps = {
|
||||||
@@ -102,6 +105,7 @@ ExecutionEnvironmentDetail.defaultProps = {
|
|||||||
executionEnvironment: null,
|
executionEnvironment: null,
|
||||||
virtualEnvironment: '',
|
virtualEnvironment: '',
|
||||||
verifyMissingVirtualEnv: true,
|
verifyMissingVirtualEnv: true,
|
||||||
|
helpText: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExecutionEnvironmentDetail;
|
export default ExecutionEnvironmentDetail;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
|||||||
import { bool, func, shape } from 'prop-types';
|
import { bool, func, shape } from 'prop-types';
|
||||||
import { Formik, useField, useFormikContext } from 'formik';
|
import { Formik, useField, useFormikContext } from 'formik';
|
||||||
import { t } from '@lingui/macro';
|
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 FormField, { FormSubmitError } from '../FormField';
|
||||||
import FormActionGroup from '../FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../FormActionGroup/FormActionGroup';
|
||||||
import { VariablesField } from '../CodeEditor';
|
import { VariablesField } from '../CodeEditor';
|
||||||
@@ -11,7 +11,7 @@ import { FormColumnLayout, FormFullWidthLayout } from '../FormLayout';
|
|||||||
import Popover from '../Popover';
|
import Popover from '../Popover';
|
||||||
import { required } from '../../util/validators';
|
import { required } from '../../util/validators';
|
||||||
|
|
||||||
const InventoryLookupField = () => {
|
const InventoryLookupField = ({ isDisabled }) => {
|
||||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||||
const [inventoryField, inventoryMeta, inventoryHelpers] = useField(
|
const [inventoryField, inventoryMeta, inventoryHelpers] = useField(
|
||||||
'inventory'
|
'inventory'
|
||||||
@@ -25,6 +25,23 @@ const InventoryLookupField = () => {
|
|||||||
[setFieldValue, setFieldTouched]
|
[setFieldValue, setFieldTouched]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderInventoryLookup = (
|
||||||
|
<InventoryLookup
|
||||||
|
fieldId="inventory-lookup"
|
||||||
|
value={inventoryField.value}
|
||||||
|
onBlur={() => 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 (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t`Inventory`}
|
label={t`Inventory`}
|
||||||
@@ -40,19 +57,13 @@ const InventoryLookupField = () => {
|
|||||||
}
|
}
|
||||||
helperTextInvalid={inventoryMeta.error}
|
helperTextInvalid={inventoryMeta.error}
|
||||||
>
|
>
|
||||||
<InventoryLookup
|
{isDisabled ? (
|
||||||
fieldId="inventory-lookup"
|
<Tooltip content={t`Unable to change inventory on a host`}>
|
||||||
value={inventoryField.value}
|
{renderInventoryLookup}
|
||||||
onBlur={() => inventoryHelpers.setTouched()}
|
</Tooltip>
|
||||||
tooltip={t`Select the inventory that this host will belong to.`}
|
) : (
|
||||||
isValid={!inventoryMeta.touched || !inventoryMeta.error}
|
renderInventoryLookup
|
||||||
helperTextInvalid={inventoryMeta.error}
|
)}
|
||||||
onChange={handleInventoryUpdate}
|
|
||||||
required
|
|
||||||
touched={inventoryMeta.touched}
|
|
||||||
error={inventoryMeta.error}
|
|
||||||
validate={required(t`Select a value for this field`)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -63,6 +74,7 @@ const HostForm = ({
|
|||||||
host,
|
host,
|
||||||
isInventoryVisible,
|
isInventoryVisible,
|
||||||
submitError,
|
submitError,
|
||||||
|
disableInventoryLookup,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
@@ -91,7 +103,9 @@ const HostForm = ({
|
|||||||
type="text"
|
type="text"
|
||||||
label={t`Description`}
|
label={t`Description`}
|
||||||
/>
|
/>
|
||||||
{isInventoryVisible && <InventoryLookupField />}
|
{isInventoryVisible && (
|
||||||
|
<InventoryLookupField isDisabled={disableInventoryLookup} />
|
||||||
|
)}
|
||||||
<FormFullWidthLayout>
|
<FormFullWidthLayout>
|
||||||
<VariablesField
|
<VariablesField
|
||||||
id="host-variables"
|
id="host-variables"
|
||||||
@@ -117,6 +131,7 @@ HostForm.propTypes = {
|
|||||||
host: shape({}),
|
host: shape({}),
|
||||||
isInventoryVisible: bool,
|
isInventoryVisible: bool,
|
||||||
submitError: shape({}),
|
submitError: shape({}),
|
||||||
|
disableInventoryLookup: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
HostForm.defaultProps = {
|
HostForm.defaultProps = {
|
||||||
@@ -131,6 +146,7 @@ HostForm.defaultProps = {
|
|||||||
},
|
},
|
||||||
isInventoryVisible: true,
|
isInventoryVisible: true,
|
||||||
submitError: null,
|
submitError: null,
|
||||||
|
disableInventoryLookup: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { HostForm as _HostForm };
|
export { HostForm as _HostForm };
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe('<HostForm />', () => {
|
|||||||
expect(wrapper.find('input#host-description').prop('value')).toEqual(
|
expect(wrapper.find('input#host-description').prop('value')).toEqual(
|
||||||
'new bar'
|
'new bar'
|
||||||
);
|
);
|
||||||
|
expect(wrapper.find('InventoryLookup').prop('isDisabled')).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls handleSubmit when form submitted', async () => {
|
test('calls handleSubmit when form submitted', async () => {
|
||||||
@@ -84,4 +85,18 @@ describe('<HostForm />', () => {
|
|||||||
});
|
});
|
||||||
expect(wrapper.find('InventoryLookupField').length).toBe(0);
|
expect(wrapper.find('InventoryLookupField').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('inventory lookup field should be disabled', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<HostForm
|
||||||
|
host={mockData}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
disableInventoryLookup
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('InventoryLookup').prop('isDisabled')).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ChipGroup from '../ChipGroup';
|
|||||||
import CredentialChip from '../CredentialChip';
|
import CredentialChip from '../CredentialChip';
|
||||||
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
|
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
|
||||||
import { formatDateString } from '../../util/dates';
|
import { formatDateString } from '../../util/dates';
|
||||||
|
import { isJobRunning } from '../../util/jobs';
|
||||||
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
||||||
import JobCancelButton from '../JobCancelButton';
|
import JobCancelButton from '../JobCancelButton';
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ function JobListItem({
|
|||||||
const jobTypes = {
|
const jobTypes = {
|
||||||
project_update: t`Source Control Update`,
|
project_update: t`Source Control Update`,
|
||||||
inventory_update: t`Inventory Sync`,
|
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`,
|
ad_hoc_command: t`Command`,
|
||||||
system_job: t`Management Job`,
|
system_job: t`Management Job`,
|
||||||
workflow_job: t`Workflow Job`,
|
workflow_job: t`Workflow Job`,
|
||||||
@@ -202,10 +203,12 @@ function JobListItem({
|
|||||||
dataCy={`job-${job.id}-project`}
|
dataCy={`job-${job.id}-project`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ExecutionEnvironmentDetail
|
{job.type !== 'workflow_job' && !isJobRunning(job.status) && (
|
||||||
executionEnvironment={execution_environment}
|
<ExecutionEnvironmentDetail
|
||||||
verifyMissingVirtualEnv={false}
|
executionEnvironment={execution_environment}
|
||||||
/>
|
verifyMissingVirtualEnv={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{credentials && credentials.length > 0 && (
|
{credentials && credentials.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ function InventoryLookup({
|
|||||||
isOverrideDisabled,
|
isOverrideDisabled,
|
||||||
validate,
|
validate,
|
||||||
fieldName,
|
fieldName,
|
||||||
|
isDisabled,
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
@@ -105,7 +106,7 @@ function InventoryLookup({
|
|||||||
label={t`Inventory`}
|
label={t`Inventory`}
|
||||||
promptId={promptId}
|
promptId={promptId}
|
||||||
promptName={promptName}
|
promptName={promptName}
|
||||||
isDisabled={!canEdit}
|
isDisabled={!canEdit || isDisabled}
|
||||||
tooltip={t`Select the inventory containing the hosts
|
tooltip={t`Select the inventory containing the hosts
|
||||||
you want this job to manage.`}
|
you want this job to manage.`}
|
||||||
>
|
>
|
||||||
@@ -120,7 +121,7 @@ function InventoryLookup({
|
|||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isDisabled={!canEdit}
|
isDisabled={!canEdit || isDisabled}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
<OptionsList
|
<OptionsList
|
||||||
@@ -176,7 +177,7 @@ function InventoryLookup({
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
required={required}
|
required={required}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isDisabled={!canEdit}
|
isDisabled={!canEdit || isDisabled}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
<OptionsList
|
<OptionsList
|
||||||
@@ -228,6 +229,7 @@ InventoryLookup.propTypes = {
|
|||||||
isOverrideDisabled: bool,
|
isOverrideDisabled: bool,
|
||||||
validate: func,
|
validate: func,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
|
isDisabled: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
InventoryLookup.defaultProps = {
|
InventoryLookup.defaultProps = {
|
||||||
@@ -236,6 +238,7 @@ InventoryLookup.defaultProps = {
|
|||||||
isOverrideDisabled: false,
|
isOverrideDisabled: false,
|
||||||
validate: () => {},
|
validate: () => {},
|
||||||
fieldName: 'inventory',
|
fieldName: 'inventory',
|
||||||
|
isDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(InventoryLookup);
|
export default withRouter(InventoryLookup);
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ Lookup.propTypes = {
|
|||||||
fieldName: string.isRequired,
|
fieldName: string.isRequired,
|
||||||
validate: func,
|
validate: func,
|
||||||
onDebounce: func,
|
onDebounce: func,
|
||||||
|
isDisabled: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
Lookup.defaultProps = {
|
Lookup.defaultProps = {
|
||||||
@@ -235,6 +236,7 @@ Lookup.defaultProps = {
|
|||||||
),
|
),
|
||||||
validate: () => undefined,
|
validate: () => undefined,
|
||||||
onDebounce: () => undefined,
|
onDebounce: () => undefined,
|
||||||
|
isDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Lookup as _Lookup };
|
export { Lookup as _Lookup };
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import {
|
||||||
import { Chip, List, ListItem } from '@patternfly/react-core';
|
Chip,
|
||||||
|
TextList,
|
||||||
|
TextListItem,
|
||||||
|
TextListVariants,
|
||||||
|
TextListItemVariants,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import { Detail, DeletedDetail } from '../DetailList';
|
import { Detail, DeletedDetail } from '../DetailList';
|
||||||
import { VariablesDetail } from '../CodeEditor';
|
import { VariablesDetail } from '../CodeEditor';
|
||||||
import CredentialChip from '../CredentialChip';
|
import CredentialChip from '../CredentialChip';
|
||||||
@@ -44,14 +48,28 @@ function PromptInventorySourceDetail({ resource }) {
|
|||||||
update_on_project_update
|
update_on_project_update
|
||||||
) {
|
) {
|
||||||
optionsList = (
|
optionsList = (
|
||||||
<List>
|
<TextList component={TextListVariants.ul}>
|
||||||
{overwrite && <ListItem>{t`Overwrite`}</ListItem>}
|
{overwrite && (
|
||||||
{overwrite_vars && <ListItem>{t`Overwrite Variables`}</ListItem>}
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{update_on_launch && <ListItem>{t`Update on Launch`}</ListItem>}
|
{t`Overwrite local groups and hosts from remote inventory source`}
|
||||||
{update_on_project_update && (
|
</TextListItem>
|
||||||
<ListItem>{t`Update on Project Update`}</ListItem>
|
|
||||||
)}
|
)}
|
||||||
</List>
|
{overwrite_vars && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Overwrite local variables from remote inventory source`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
{update_on_launch && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Update on launch`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
{update_on_project_update && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Update on project update`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
</TextList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +180,9 @@ function PromptInventorySourceDetail({ resource }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{optionsList && <Detail label={t`Options`} value={optionsList} />}
|
{optionsList && (
|
||||||
|
<Detail fullWidth label={t`Enabled Options`} value={optionsList} />
|
||||||
|
)}
|
||||||
{source_vars && (
|
{source_vars && (
|
||||||
<VariablesDetail
|
<VariablesDetail
|
||||||
label={t`Source Variables`}
|
label={t`Source Variables`}
|
||||||
|
|||||||
@@ -60,12 +60,14 @@ describe('PromptInventorySourceDetail', () => {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('Detail[label="Options"]')
|
.find('Detail[label="Enabled Options"]')
|
||||||
.containsAllMatchingElements([
|
.containsAllMatchingElements([
|
||||||
<li>Overwrite</li>,
|
<li>
|
||||||
<li>Overwrite Variables</li>,
|
Overwrite local groups and hosts from remote inventory source
|
||||||
<li>Update on Launch</li>,
|
</li>,
|
||||||
<li>Update on Project Update</li>,
|
<li>Overwrite local variables from remote inventory source</li>,
|
||||||
|
<li>Update on launch</li>,
|
||||||
|
<li>Update on project update</li>,
|
||||||
])
|
])
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import {
|
||||||
import { Chip, List, ListItem } from '@patternfly/react-core';
|
Chip,
|
||||||
|
TextList,
|
||||||
|
TextListItem,
|
||||||
|
TextListVariants,
|
||||||
|
TextListItemVariants,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import CredentialChip from '../CredentialChip';
|
import CredentialChip from '../CredentialChip';
|
||||||
import ChipGroup from '../ChipGroup';
|
import ChipGroup from '../ChipGroup';
|
||||||
import Sparkline from '../Sparkline';
|
import Sparkline from '../Sparkline';
|
||||||
@@ -51,19 +55,37 @@ function PromptJobTemplateDetail({ resource }) {
|
|||||||
become_enabled ||
|
become_enabled ||
|
||||||
host_config_key ||
|
host_config_key ||
|
||||||
allow_simultaneous ||
|
allow_simultaneous ||
|
||||||
use_fact_cache
|
use_fact_cache ||
|
||||||
|
webhook_service
|
||||||
) {
|
) {
|
||||||
optionsList = (
|
optionsList = (
|
||||||
<List>
|
<TextList component={TextListVariants.ul}>
|
||||||
{become_enabled && (
|
{become_enabled && (
|
||||||
<ListItem>{t`Enable Privilege Escalation`}</ListItem>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Privilege Escalation`}
|
||||||
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
{host_config_key && (
|
{host_config_key && (
|
||||||
<ListItem>{t`Allow Provisioning Callbacks`}</ListItem>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Provisioning Callbacks`}
|
||||||
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
{allow_simultaneous && <ListItem>{t`Enable Concurrent Jobs`}</ListItem>}
|
{allow_simultaneous && (
|
||||||
{use_fact_cache && <ListItem>{t`Use Fact Storage`}</ListItem>}
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
</List>
|
{t`Concurrent Jobs`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
{use_fact_cache && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Fact Storage`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
{webhook_service && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Webhooks`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
</TextList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +186,7 @@ function PromptJobTemplateDetail({ resource }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{optionsList && <Detail label={t`Options`} value={optionsList} />}
|
{optionsList && <Detail label={t`Enabled Options`} value={optionsList} />}
|
||||||
{summary_fields?.credentials?.length > 0 && (
|
{summary_fields?.credentials?.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -96,12 +96,13 @@ describe('PromptJobTemplateDetail', () => {
|
|||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('Detail[label="Options"]')
|
.find('Detail[label="Enabled Options"]')
|
||||||
.containsAllMatchingElements([
|
.containsAllMatchingElements([
|
||||||
<li>Enable Privilege Escalation</li>,
|
<li>Privilege Escalation</li>,
|
||||||
<li>Allow Provisioning Callbacks</li>,
|
<li>Provisioning Callbacks</li>,
|
||||||
<li>Enable Concurrent Jobs</li>,
|
<li>Concurrent Jobs</li>,
|
||||||
<li>Use Fact Storage</li>,
|
<li>Fact Storage</li>,
|
||||||
|
<li>Webhooks</li>,
|
||||||
])
|
])
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
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 { Link } from 'react-router-dom';
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
|
|
||||||
import { Detail, DeletedDetail } from '../DetailList';
|
import { Detail, DeletedDetail } from '../DetailList';
|
||||||
import CredentialChip from '../CredentialChip';
|
import CredentialChip from '../CredentialChip';
|
||||||
import { toTitleCase } from '../../util/strings';
|
import { toTitleCase } from '../../util/strings';
|
||||||
@@ -36,17 +39,33 @@ function PromptProjectDetail({ resource }) {
|
|||||||
allow_override
|
allow_override
|
||||||
) {
|
) {
|
||||||
optionsList = (
|
optionsList = (
|
||||||
<List>
|
<TextList component={TextListVariants.ul}>
|
||||||
{scm_clean && <ListItem>{t`Clean`}</ListItem>}
|
{scm_clean && (
|
||||||
{scm_delete_on_update && <ListItem>{t`Delete on Update`}</ListItem>}
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Discard local changes before syncing`}</TextListItem>
|
||||||
|
)}
|
||||||
|
{scm_delete_on_update && (
|
||||||
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Delete the project before syncing`}</TextListItem>
|
||||||
|
)}
|
||||||
{scm_track_submodules && (
|
{scm_track_submodules && (
|
||||||
<ListItem>{t`Track submodules latest commit on branch`}</ListItem>
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Track submodules latest commit on branch`}</TextListItem>
|
||||||
)}
|
)}
|
||||||
{scm_update_on_launch && (
|
{scm_update_on_launch && (
|
||||||
<ListItem>{t`Update Revision on Launch`}</ListItem>
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Update revision on job launch`}</TextListItem>
|
||||||
)}
|
)}
|
||||||
{allow_override && <ListItem>{t`Allow Branch Override`}</ListItem>}
|
{allow_override && (
|
||||||
</List>
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Allow branch override`}</TextListItem>
|
||||||
|
)}
|
||||||
|
</TextList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +109,7 @@ function PromptProjectDetail({ resource }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{optionsList && <Detail label={t`Options`} value={optionsList} />}
|
{optionsList && <Detail label={t`Enabled Options`} value={optionsList} />}
|
||||||
<Detail
|
<Detail
|
||||||
label={t`Cache Timeout`}
|
label={t`Cache Timeout`}
|
||||||
value={`${scm_update_cache_timeout} ${t`Seconds`}`}
|
value={`${scm_update_cache_timeout} ${t`Seconds`}`}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ describe('PromptProjectDetail', () => {
|
|||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<PromptProjectDetail resource={mockProject} />,
|
<PromptProjectDetail
|
||||||
|
resource={{ ...mockProject, scm_track_submodules: true }}
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
context: { config },
|
context: { config },
|
||||||
}
|
}
|
||||||
@@ -54,12 +56,13 @@ describe('PromptProjectDetail', () => {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('Detail[label="Options"]')
|
.find('Detail[label="Enabled Options"]')
|
||||||
.containsAllMatchingElements([
|
.containsAllMatchingElements([
|
||||||
<li>Clean</li>,
|
<li>Discard local changes before syncing</li>,
|
||||||
<li>Delete on Update</li>,
|
<li>Delete the project before syncing</li>,
|
||||||
<li>Update Revision on Launch</li>,
|
<li>Track submodules latest commit on branch</li>,
|
||||||
<li>Allow Branch Override</li>,
|
<li>Update revision on job launch</li>,
|
||||||
|
<li>Allow branch override</li>,
|
||||||
])
|
])
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import {
|
||||||
import { Chip, List, ListItem } from '@patternfly/react-core';
|
Chip,
|
||||||
|
TextList,
|
||||||
|
TextListItem,
|
||||||
|
TextListVariants,
|
||||||
|
TextListItemVariants,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import CredentialChip from '../CredentialChip';
|
import CredentialChip from '../CredentialChip';
|
||||||
import ChipGroup from '../ChipGroup';
|
import ChipGroup from '../ChipGroup';
|
||||||
import { Detail } from '../DetailList';
|
import { Detail } from '../DetailList';
|
||||||
@@ -26,10 +30,18 @@ function PromptWFJobTemplateDetail({ resource }) {
|
|||||||
let optionsList = '';
|
let optionsList = '';
|
||||||
if (allow_simultaneous || webhook_service) {
|
if (allow_simultaneous || webhook_service) {
|
||||||
optionsList = (
|
optionsList = (
|
||||||
<List>
|
<TextList component={TextListVariants.ul}>
|
||||||
{allow_simultaneous && <ListItem>{t`Enable Concurrent Jobs`}</ListItem>}
|
{allow_simultaneous && (
|
||||||
{webhook_service && <ListItem>{t`Enable Webhooks`}</ListItem>}
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
</List>
|
{t`Concurrent Jobs`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
{webhook_service && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Webhooks`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
</TextList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +94,7 @@ function PromptWFJobTemplateDetail({ resource }) {
|
|||||||
value={`${window.location.origin}${related.webhook_receiver}`}
|
value={`${window.location.origin}${related.webhook_receiver}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{optionsList && <Detail label={t`Options`} value={optionsList} />}
|
{optionsList && <Detail label={t`Enabled Options`} value={optionsList} />}
|
||||||
{summary_fields?.webhook_credential && (
|
{summary_fields?.webhook_credential && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ describe('PromptWFJobTemplateDetail', () => {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('Detail[label="Options"]')
|
.find('Detail[label="Enabled Options"]')
|
||||||
.containsAllMatchingElements([
|
.containsAllMatchingElements([
|
||||||
<li>Enable Concurrent Jobs</li>,
|
<li>Concurrent Jobs</li>,
|
||||||
<li>Enable Webhooks</li>,
|
<li>Webhooks</li>,
|
||||||
])
|
])
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useRouteMatch } from 'react-router-dom';
|
|||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { ConfigAPI, MeAPI } from '../api';
|
import { ConfigAPI, MeAPI, UsersAPI, OrganizationsAPI } from '../api';
|
||||||
import useRequest, { useDismissableError } from '../util/useRequest';
|
import useRequest, { useDismissableError } from '../util/useRequest';
|
||||||
import AlertModal from '../components/AlertModal';
|
import AlertModal from '../components/AlertModal';
|
||||||
import ErrorDetail from '../components/ErrorDetail';
|
import ErrorDetail from '../components/ErrorDetail';
|
||||||
@@ -35,9 +35,32 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
|
] = 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);
|
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 = () => {
|
export const useAuthorizedPath = () => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const subscriptionMgmtRoute = useRouteMatch({
|
const subscriptionMgmtRoute = useRouteMatch({
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import Users from './screens/User';
|
|||||||
import WorkflowApprovals from './screens/WorkflowApproval';
|
import WorkflowApprovals from './screens/WorkflowApproval';
|
||||||
import { Jobs } from './screens/Job';
|
import { Jobs } from './screens/Job';
|
||||||
|
|
||||||
function getRouteConfig() {
|
function getRouteConfig(userProfile = {}) {
|
||||||
return [
|
let routeConfig = [
|
||||||
{
|
{
|
||||||
groupTitle: <Trans>Views</Trans>,
|
groupTitle: <Trans>Views</Trans>,
|
||||||
groupId: 'views_group',
|
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;
|
export default getRouteConfig;
|
||||||
|
|||||||
248
awx/ui_next/src/routeConfig.test.jsx
Normal file
248
awx/ui_next/src/routeConfig.test.jsx
Normal file
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import React, { Fragment, useEffect, useCallback } from 'react';
|
import React, { Fragment, useEffect, useCallback } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
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 AlertModal from '../../../components/AlertModal';
|
||||||
import { CardBody, CardActionsRow } from '../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
@@ -134,15 +139,7 @@ function CredentialDetail({ credential }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean') {
|
||||||
return (
|
return null;
|
||||||
<Detail
|
|
||||||
dataCy={`credential-${id}-detail`}
|
|
||||||
id={`credential-${id}-detail`}
|
|
||||||
key={id}
|
|
||||||
label={t`Options`}
|
|
||||||
value={<List>{inputs[id] && <ListItem>{label}</ListItem>}</List>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputs[id] === '$encrypted$') {
|
if (inputs[id] === '$encrypted$') {
|
||||||
@@ -189,6 +186,10 @@ function CredentialDetail({ credential }) {
|
|||||||
credential
|
credential
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enabledBooleanFields = fields.filter(
|
||||||
|
({ id, type }) => type === 'boolean' && inputs[id]
|
||||||
|
);
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
@@ -255,6 +256,20 @@ function CredentialDetail({ credential }) {
|
|||||||
date={modified}
|
date={modified}
|
||||||
user={modified_by}
|
user={modified_by}
|
||||||
/>
|
/>
|
||||||
|
{enabledBooleanFields.length > 0 && (
|
||||||
|
<Detail
|
||||||
|
label={t`Enabled Options`}
|
||||||
|
value={
|
||||||
|
<TextList component={TextListVariants.ul}>
|
||||||
|
{enabledBooleanFields.map(({ id, label }) => (
|
||||||
|
<TextListItem key={id} component={TextListItemVariants.li}>
|
||||||
|
{label}
|
||||||
|
</TextListItem>
|
||||||
|
))}
|
||||||
|
</TextList>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DetailList>
|
</DetailList>
|
||||||
{Object.keys(inputSources).length > 0 && (
|
{Object.keys(inputSources).length > 0 && (
|
||||||
<PluginFieldText>
|
<PluginFieldText>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ describe('<CredentialDetail />', () => {
|
|||||||
'Privilege Escalation Password',
|
'Privilege Escalation Password',
|
||||||
'Prompt on launch'
|
'Prompt on launch'
|
||||||
);
|
);
|
||||||
expect(wrapper.find(`Detail[label="Options"] ListItem`).text()).toEqual(
|
expect(wrapper.find(`Detail[label="Enabled Options"] li`).text()).toEqual(
|
||||||
'Authorize'
|
'Authorize'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function HostEdit({ host }) {
|
|||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
submitError={formError}
|
submitError={formError}
|
||||||
|
disableInventoryLookup
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -141,8 +141,10 @@ function InstanceList() {
|
|||||||
[instanceGroupId]
|
[instanceGroupId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const readInstancesOptions = () =>
|
const readInstancesOptions = useCallback(
|
||||||
InstanceGroupsAPI.readInstanceOptions(instanceGroupId);
|
() => InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
|
||||||
|
[instanceGroupId]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
import { Button, List, ListItem } from '@patternfly/react-core';
|
Button,
|
||||||
|
TextList,
|
||||||
|
TextListItem,
|
||||||
|
TextListVariants,
|
||||||
|
TextListItemVariants,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import { CardBody, CardActionsRow } from '../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
import { VariablesDetail } from '../../../components/CodeEditor';
|
import { VariablesDetail } from '../../../components/CodeEditor';
|
||||||
@@ -19,7 +23,6 @@ import {
|
|||||||
UserDateDetail,
|
UserDateDetail,
|
||||||
} from '../../../components/DetailList';
|
} from '../../../components/DetailList';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import Popover from '../../../components/Popover';
|
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import { InventorySourcesAPI } from '../../../api';
|
import { InventorySourcesAPI } from '../../../api';
|
||||||
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
@@ -112,71 +115,28 @@ function InventorySourceDetail({ inventorySource }) {
|
|||||||
update_on_project_update
|
update_on_project_update
|
||||||
) {
|
) {
|
||||||
optionsList = (
|
optionsList = (
|
||||||
<List>
|
<TextList component={TextListVariants.ul}>
|
||||||
{overwrite && (
|
{overwrite && (
|
||||||
<ListItem>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Overwrite`}
|
{t`Overwrite local groups and hosts from remote inventory source`}
|
||||||
<Popover
|
</TextListItem>
|
||||||
content={
|
|
||||||
<>
|
|
||||||
{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.`}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{t`When not checked, local child
|
|
||||||
hosts and groups not found on the external source will remain
|
|
||||||
untouched by the inventory update process.`}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
)}
|
||||||
{overwrite_vars && (
|
{overwrite_vars && (
|
||||||
<ListItem>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Overwrite variables`}
|
{t`Overwrite local variables from remote inventory source`}
|
||||||
<Popover
|
</TextListItem>
|
||||||
content={
|
|
||||||
<>
|
|
||||||
{t`If checked, all variables for child groups
|
|
||||||
and hosts will be removed and replaced by those found
|
|
||||||
on the external source.`}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{t`When not checked, a merge will be performed,
|
|
||||||
combining local variables with those found on the
|
|
||||||
external source.`}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
)}
|
||||||
{update_on_launch && (
|
{update_on_launch && (
|
||||||
<ListItem>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Update on launch`}
|
{t`Update on launch`}
|
||||||
<Popover
|
</TextListItem>
|
||||||
content={t`Each time a job runs using this inventory,
|
|
||||||
refresh the inventory from the selected source before
|
|
||||||
executing job tasks.`}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
)}
|
||||||
{update_on_project_update && (
|
{update_on_project_update && (
|
||||||
<ListItem>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Update on project update`}
|
{t`Update on project update`}
|
||||||
<Popover
|
</TextListItem>
|
||||||
content={t`After every project update where the SCM revision
|
|
||||||
changes, refresh the inventory from the selected source
|
|
||||||
before executing job tasks. This is intended for static content,
|
|
||||||
like the Ansible inventory .ini file format.`}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
)}
|
||||||
</List>
|
</TextList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +202,7 @@ function InventorySourceDetail({ inventorySource }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{optionsList && (
|
{optionsList && (
|
||||||
<Detail fullWidth label={t`Options`} value={optionsList} />
|
<Detail fullWidth label={t`Enabled Options`} value={optionsList} />
|
||||||
)}
|
)}
|
||||||
{source_vars && (
|
{source_vars && (
|
||||||
<VariablesDetail
|
<VariablesDetail
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ describe('InventorySourceDetail', () => {
|
|||||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||||
'---\nfoo: bar'
|
'---\nfoo: bar'
|
||||||
);
|
);
|
||||||
wrapper.find('Detail[label="Options"] li').forEach(option => {
|
wrapper.find('Detail[label="Enabled Options"] li').forEach(option => {
|
||||||
expect([
|
expect([
|
||||||
'Overwrite',
|
'Overwrite local groups and hosts from remote inventory source',
|
||||||
'Overwrite variables',
|
'Overwrite local variables from remote inventory source',
|
||||||
'Update on launch',
|
'Update on launch',
|
||||||
'Update on project update',
|
'Update on project update',
|
||||||
]).toContain(option.text());
|
]).toContain(option.text());
|
||||||
|
|||||||
@@ -28,25 +28,22 @@ function SmartInventory({ setBreadcrumb }) {
|
|||||||
const match = useRouteMatch('/inventories/smart_inventory/:id');
|
const match = useRouteMatch('/inventories/smart_inventory/:id');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { inventory },
|
result: inventory,
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading: hasContentLoading,
|
isLoading: hasContentLoading,
|
||||||
request: fetchInventory,
|
request: fetchInventory,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const { data } = await InventoriesAPI.readDetail(match.params.id);
|
const { data } = await InventoriesAPI.readDetail(match.params.id);
|
||||||
return {
|
return data;
|
||||||
inventory: data,
|
|
||||||
};
|
|
||||||
}, [match.params.id]),
|
}, [match.params.id]),
|
||||||
{
|
|
||||||
inventory: null,
|
null
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInventory();
|
fetchInventory();
|
||||||
}, [fetchInventory]);
|
}, [fetchInventory, location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inventory) {
|
if (inventory) {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function JobDetail({ job }) {
|
|||||||
inventory_update: t`Inventory Sync`,
|
inventory_update: t`Inventory Sync`,
|
||||||
job: job.job_type === 'check' ? t`Playbook Check` : t`Playbook Run`,
|
job: job.job_type === 'check' ? t`Playbook Check` : t`Playbook Run`,
|
||||||
ad_hoc_command: t`Command`,
|
ad_hoc_command: t`Command`,
|
||||||
management_job: t`Management Job`,
|
system_job: t`Management Job`,
|
||||||
workflow_job: t`Workflow Job`,
|
workflow_job: t`Workflow Job`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,10 +220,12 @@ function JobDetail({ job }) {
|
|||||||
<Detail label={t`Playbook`} value={job.playbook} />
|
<Detail label={t`Playbook`} value={job.playbook} />
|
||||||
<Detail label={t`Limit`} value={job.limit} />
|
<Detail label={t`Limit`} value={job.limit} />
|
||||||
<Detail label={t`Verbosity`} value={VERBOSITY[job.verbosity]} />
|
<Detail label={t`Verbosity`} value={VERBOSITY[job.verbosity]} />
|
||||||
<ExecutionEnvironmentDetail
|
{job.type !== 'workflow_job' && !isJobRunning(job.status) && (
|
||||||
executionEnvironment={executionEnvironment}
|
<ExecutionEnvironmentDetail
|
||||||
verifyMissingVirtualEnv={false}
|
executionEnvironment={executionEnvironment}
|
||||||
/>
|
verifyMissingVirtualEnv={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Detail label={t`Execution Node`} value={job.execution_node} />
|
<Detail label={t`Execution Node`} value={job.execution_node} />
|
||||||
{instanceGroup && !instanceGroup?.is_container_group && (
|
{instanceGroup && !instanceGroup?.is_container_group && (
|
||||||
<Detail
|
<Detail
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { shape } from 'prop-types';
|
import { shape } from 'prop-types';
|
||||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||||
import { CompassIcon, WrenchIcon } from '@patternfly/react-icons';
|
|
||||||
|
import {
|
||||||
|
CompassIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
ProjectDiagramIcon,
|
||||||
|
} from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import StatusIcon from '../../../components/StatusIcon';
|
import StatusIcon from '../../../components/StatusIcon';
|
||||||
import {
|
import {
|
||||||
@@ -58,11 +63,15 @@ const StatusIconWithMargin = styled(StatusIcon)`
|
|||||||
|
|
||||||
function WorkflowOutputToolbar({ job }) {
|
function WorkflowOutputToolbar({ job }) {
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
const history = useHistory();
|
||||||
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
|
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
|
||||||
|
|
||||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||||
|
const navToWorkflow = () => {
|
||||||
|
history.push(
|
||||||
|
`/templates/workflow_job_template/${job.unified_job_template}/visualizer`
|
||||||
|
);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Toolbar id="workflow-output-toolbar">
|
<Toolbar id="workflow-output-toolbar">
|
||||||
<ToolbarJob>
|
<ToolbarJob>
|
||||||
@@ -70,6 +79,15 @@ function WorkflowOutputToolbar({ job }) {
|
|||||||
<b>{job.name}</b>
|
<b>{job.name}</b>
|
||||||
</ToolbarJob>
|
</ToolbarJob>
|
||||||
<ToolbarActions>
|
<ToolbarActions>
|
||||||
|
<ActionButton
|
||||||
|
ouiaId="edit-workflow"
|
||||||
|
aria-label={t`Edit workflow`}
|
||||||
|
id="edit-workflow"
|
||||||
|
variant="plain"
|
||||||
|
onClick={navToWorkflow}
|
||||||
|
>
|
||||||
|
<ProjectDiagramIcon />
|
||||||
|
</ActionButton>
|
||||||
<div>{t`Total Nodes`}</div>
|
<div>{t`Total Nodes`}</div>
|
||||||
<Badge isRead>{totalNodes}</Badge>
|
<Badge isRead>{totalNodes}</Badge>
|
||||||
<Tooltip content={t`Toggle Legend`} position="bottom">
|
<Tooltip content={t`Toggle Legend`} position="bottom">
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ const workflowContext = {
|
|||||||
showTools: false,
|
showTools: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function shouldFind(element) {
|
||||||
|
expect(wrapper.find(element)).toHaveLength(1);
|
||||||
|
}
|
||||||
describe('WorkflowOutputToolbar', () => {
|
describe('WorkflowOutputToolbar', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
const nodes = [
|
const nodes = [
|
||||||
@@ -45,6 +48,13 @@ describe('WorkflowOutputToolbar', () => {
|
|||||||
wrapper.unmount();
|
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', () => {
|
test('Shows correct number of nodes', () => {
|
||||||
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
||||||
expect(wrapper.find('Badge').text()).toBe('1');
|
expect(wrapper.find('Badge').text()).toBe('1');
|
||||||
|
|||||||
@@ -133,11 +133,13 @@ function OrganizationDetail({ organization }) {
|
|||||||
value={
|
value={
|
||||||
<ChipGroup numChips={5} totalChips={galaxy_credentials.length}>
|
<ChipGroup numChips={5} totalChips={galaxy_credentials.length}>
|
||||||
{galaxy_credentials.map(credential => (
|
{galaxy_credentials.map(credential => (
|
||||||
<CredentialChip
|
<Link to={`/credentials/${credential.id}/details`}>
|
||||||
credential={credential}
|
<CredentialChip
|
||||||
key={credential.id}
|
credential={credential}
|
||||||
isReadOnly
|
key={credential.id}
|
||||||
/>
|
isReadOnly
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</ChipGroup>
|
</ChipGroup>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import styled from 'styled-components';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
List,
|
TextList,
|
||||||
ListItem,
|
TextListItem,
|
||||||
|
TextListVariants,
|
||||||
|
TextListItemVariants,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { Project } from '../../../types';
|
import { Project } from '../../../types';
|
||||||
@@ -78,17 +80,33 @@ function ProjectDetail({ project }) {
|
|||||||
allow_override
|
allow_override
|
||||||
) {
|
) {
|
||||||
optionsList = (
|
optionsList = (
|
||||||
<List>
|
<TextList component={TextListVariants.ul}>
|
||||||
{scm_clean && <ListItem>{t`Clean`}</ListItem>}
|
{scm_clean && (
|
||||||
{scm_delete_on_update && <ListItem>{t`Delete on Update`}</ListItem>}
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Discard local changes before syncing`}</TextListItem>
|
||||||
|
)}
|
||||||
|
{scm_delete_on_update && (
|
||||||
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Delete the project before syncing`}</TextListItem>
|
||||||
|
)}
|
||||||
{scm_track_submodules && (
|
{scm_track_submodules && (
|
||||||
<ListItem>{t`Track submodules latest commit on branch`}</ListItem>
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Track submodules latest commit on branch`}</TextListItem>
|
||||||
)}
|
)}
|
||||||
{scm_update_on_launch && (
|
{scm_update_on_launch && (
|
||||||
<ListItem>{t`Update Revision on Launch`}</ListItem>
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Update revision on job launch`}</TextListItem>
|
||||||
)}
|
)}
|
||||||
{allow_override && <ListItem>{t`Allow Branch Override`}</ListItem>}
|
{allow_override && (
|
||||||
</List>
|
<TextListItem
|
||||||
|
component={TextListItemVariants.li}
|
||||||
|
>{t`Allow branch override`}</TextListItem>
|
||||||
|
)}
|
||||||
|
</TextList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +214,6 @@ function ProjectDetail({ project }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{optionsList && <Detail label={t`Options`} value={optionsList} />}
|
|
||||||
<Detail
|
<Detail
|
||||||
label={t`Cache Timeout`}
|
label={t`Cache Timeout`}
|
||||||
value={`${scm_update_cache_timeout} ${t`Seconds`}`}
|
value={`${scm_update_cache_timeout} ${t`Seconds`}`}
|
||||||
@@ -212,7 +229,6 @@ function ProjectDetail({ project }) {
|
|||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
<Detail label={t`Playbook Directory`} value={local_path} />
|
<Detail label={t`Playbook Directory`} value={local_path} />
|
||||||
|
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={t`Created`}
|
label={t`Created`}
|
||||||
date={created}
|
date={created}
|
||||||
@@ -223,6 +239,9 @@ function ProjectDetail({ project }) {
|
|||||||
date={modified}
|
date={modified}
|
||||||
user={summary_fields.modified_by}
|
user={summary_fields.modified_by}
|
||||||
/>
|
/>
|
||||||
|
{optionsList && (
|
||||||
|
<Detail fullWidth label={t`Enabled Options`} value={optionsList} />
|
||||||
|
)}
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
{summary_fields.user_capabilities?.edit && (
|
{summary_fields.user_capabilities?.edit && (
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe('<ProjectDetail />', () => {
|
|||||||
scm_refspec: 'refs/remotes/*',
|
scm_refspec: 'refs/remotes/*',
|
||||||
scm_clean: true,
|
scm_clean: true,
|
||||||
scm_delete_on_update: true,
|
scm_delete_on_update: true,
|
||||||
scm_track_submodules: false,
|
scm_track_submodules: true,
|
||||||
credential: 100,
|
credential: 100,
|
||||||
status: 'successful',
|
status: 'successful',
|
||||||
organization: 10,
|
organization: 10,
|
||||||
@@ -127,12 +127,13 @@ describe('<ProjectDetail />', () => {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('Detail[label="Options"]')
|
.find('Detail[label="Enabled Options"]')
|
||||||
.containsAllMatchingElements([
|
.containsAllMatchingElements([
|
||||||
<li>Clean</li>,
|
<li>Discard local changes before syncing</li>,
|
||||||
<li>Delete on Update</li>,
|
<li>Delete the project before syncing</li>,
|
||||||
<li>Update Revision on Launch</li>,
|
<li>Track submodules latest commit on branch</li>,
|
||||||
<li>Allow Branch Override</li>,
|
<li>Update revision on job launch</li>,
|
||||||
|
<li>Allow branch override</li>,
|
||||||
])
|
])
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
});
|
});
|
||||||
@@ -151,7 +152,7 @@ describe('<ProjectDetail />', () => {
|
|||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<ProjectDetail project={{ ...mockProject, ...mockOptions }} />
|
<ProjectDetail project={{ ...mockProject, ...mockOptions }} />
|
||||||
);
|
);
|
||||||
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', () => {
|
test('should have proper number of delete detail requests', () => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from '../../../../util/dates';
|
} from '../../../../util/dates';
|
||||||
|
|
||||||
function SubscriptionDetail() {
|
function SubscriptionDetail() {
|
||||||
const { license_info, version } = useConfig();
|
const { me = {}, license_info, version } = useConfig();
|
||||||
const baseURL = '/settings/subscription';
|
const baseURL = '/settings/subscription';
|
||||||
const tabsArray = [
|
const tabsArray = [
|
||||||
{
|
{
|
||||||
@@ -164,15 +164,17 @@ function SubscriptionDetail() {
|
|||||||
contact us.
|
contact us.
|
||||||
</Button>
|
</Button>
|
||||||
</Trans>
|
</Trans>
|
||||||
<CardActionsRow>
|
{me.is_superuser && (
|
||||||
<Button
|
<CardActionsRow>
|
||||||
aria-label={t`edit`}
|
<Button
|
||||||
component={Link}
|
aria-label={t`edit`}
|
||||||
to="/settings/subscription/edit"
|
component={Link}
|
||||||
>
|
to="/settings/subscription/edit"
|
||||||
<Trans>Edit</Trans>
|
>
|
||||||
</Button>
|
<Trans>Edit</Trans>
|
||||||
</CardActionsRow>
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ describe('<SubscriptionDetail />', () => {
|
|||||||
assertDetail('Hosts remaining', '1000');
|
assertDetail('Hosts remaining', '1000');
|
||||||
assertDetail('Hosts automated', '12 since 3/2/2021, 7:43:48 PM');
|
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(<SubscriptionDetail />, {
|
||||||
|
context: { ...config, me: { is_superuser: true } },
|
||||||
|
});
|
||||||
|
|
||||||
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1);
|
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -115,28 +115,37 @@ function JobTemplateDetail({ template }) {
|
|||||||
);
|
);
|
||||||
const generateCallBackUrl = `${window.location.origin + url}callback/`;
|
const generateCallBackUrl = `${window.location.origin + url}callback/`;
|
||||||
const renderOptionsField =
|
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 = (
|
const renderOptions = (
|
||||||
<TextList component={TextListVariants.ul}>
|
<TextList component={TextListVariants.ul}>
|
||||||
{become_enabled && (
|
{become_enabled && (
|
||||||
<TextListItem component={TextListItemVariants.li}>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Enable Privilege Escalation`}
|
{t`Privilege Escalation`}
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
{host_config_key && (
|
{host_config_key && (
|
||||||
<TextListItem component={TextListItemVariants.li}>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Allow Provisioning Callbacks`}
|
{t`Provisioning Callbacks`}
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
{allow_simultaneous && (
|
{allow_simultaneous && (
|
||||||
<TextListItem component={TextListItemVariants.li}>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Enable Concurrent Jobs`}
|
{t`Concurrent Jobs`}
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
{use_fact_cache && (
|
{use_fact_cache && (
|
||||||
<TextListItem component={TextListItemVariants.li}>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`Use Fact Storage`}
|
{t`Fact Storage`}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
{webhook_service && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Webhooks`}
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
</TextList>
|
</TextList>
|
||||||
@@ -166,7 +175,6 @@ function JobTemplateDetail({ template }) {
|
|||||||
if (isLoadingInstanceGroups || isDeleteLoading) {
|
if (isLoadingInstanceGroups || isDeleteLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
@@ -212,7 +220,10 @@ function JobTemplateDetail({ template }) {
|
|||||||
)}
|
)}
|
||||||
<ExecutionEnvironmentDetail
|
<ExecutionEnvironmentDetail
|
||||||
virtualEnvironment={custom_virtualenv}
|
virtualEnvironment={custom_virtualenv}
|
||||||
executionEnvironment={summary_fields?.execution_environment}
|
executionEnvironment={summary_fields?.resolved_environment}
|
||||||
|
helpText={t`The execution environment that will be used when launching
|
||||||
|
this job template. The resolved execution environment can be overridden by
|
||||||
|
explicitly assigning a different one to this job template.`}
|
||||||
/>
|
/>
|
||||||
<Detail label={t`Source Control Branch`} value={template.scm_branch} />
|
<Detail label={t`Source Control Branch`} value={template.scm_branch} />
|
||||||
<Detail label={t`Playbook`} value={playbook} />
|
<Detail label={t`Playbook`} value={playbook} />
|
||||||
@@ -256,9 +267,6 @@ function JobTemplateDetail({ template }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{renderOptionsField && (
|
|
||||||
<Detail label={t`Options`} value={renderOptions} />
|
|
||||||
)}
|
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={t`Created`}
|
label={t`Created`}
|
||||||
date={created}
|
date={created}
|
||||||
@@ -269,6 +277,9 @@ function JobTemplateDetail({ template }) {
|
|||||||
date={modified}
|
date={modified}
|
||||||
user={summary_fields.modified_by}
|
user={summary_fields.modified_by}
|
||||||
/>
|
/>
|
||||||
|
{renderOptionsField && (
|
||||||
|
<Detail fullWidth label={t`Enabled Options`} value={renderOptions} />
|
||||||
|
)}
|
||||||
{summary_fields.credentials && summary_fields.credentials.length > 0 && (
|
{summary_fields.credentials && summary_fields.credentials.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -279,7 +290,9 @@ function JobTemplateDetail({ template }) {
|
|||||||
totalChips={summary_fields.credentials.length}
|
totalChips={summary_fields.credentials.length}
|
||||||
>
|
>
|
||||||
{summary_fields.credentials.map(c => (
|
{summary_fields.credentials.map(c => (
|
||||||
<CredentialChip key={c.id} credential={c} isReadOnly />
|
<Link to={`/credentials/${c.id}/details`}>
|
||||||
|
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</ChipGroup>
|
</ChipGroup>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ function WorkflowJobTemplateDetail({ template }) {
|
|||||||
<TextList component={TextListVariants.ul}>
|
<TextList component={TextListVariants.ul}>
|
||||||
{template.allow_simultaneous && (
|
{template.allow_simultaneous && (
|
||||||
<TextListItem component={TextListItemVariants.li}>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`- Enable Concurrent Jobs`}
|
{t`Concurrent Jobs`}
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
{template.webhook_service && (
|
{template.webhook_service && (
|
||||||
<TextListItem component={TextListItemVariants.li}>
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
{t`- Enable Webhooks`}
|
{t`Webhooks`}
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
)}
|
)}
|
||||||
</TextList>
|
</TextList>
|
||||||
@@ -186,9 +186,6 @@ function WorkflowJobTemplateDetail({ template }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{renderOptionsField && (
|
|
||||||
<Detail label={t`Options`} value={renderOptions} />
|
|
||||||
)}
|
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={t`Created`}
|
label={t`Created`}
|
||||||
date={created}
|
date={created}
|
||||||
@@ -199,6 +196,9 @@ function WorkflowJobTemplateDetail({ template }) {
|
|||||||
date={modified}
|
date={modified}
|
||||||
user={summary_fields.modified_by}
|
user={summary_fields.modified_by}
|
||||||
/>
|
/>
|
||||||
|
{renderOptionsField && (
|
||||||
|
<Detail fullWidth label={t`Enabled Options`} value={renderOptions} />
|
||||||
|
)}
|
||||||
{summary_fields.labels?.results?.length > 0 && (
|
{summary_fields.labels?.results?.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -139,6 +139,12 @@
|
|||||||
"name": "Default EE",
|
"name": "Default EE",
|
||||||
"description": "",
|
"description": "",
|
||||||
"image": "quay.io/ansible/awx-ee"
|
"image": "quay.io/ansible/awx-ee"
|
||||||
|
},
|
||||||
|
"resolved_environment": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default EE",
|
||||||
|
"description": "",
|
||||||
|
"image": "quay.io/ansible/awx-ee"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"created": "2019-09-30T16:18:34.564820Z",
|
"created": "2019-09-30T16:18:34.564820Z",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Formik, useField, useFormikContext } from 'formik';
|
import { Formik, useField, useFormikContext } from 'formik';
|
||||||
import { Form, FormGroup } from '@patternfly/react-core';
|
import { Form, FormGroup } from '@patternfly/react-core';
|
||||||
|
import { useConfig } from '../../../contexts/Config';
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
import FormField, {
|
import FormField, {
|
||||||
@@ -16,7 +17,7 @@ import { FormColumnLayout } from '../../../components/FormLayout';
|
|||||||
|
|
||||||
function UserFormFields({ user }) {
|
function UserFormFields({ user }) {
|
||||||
const { setFieldValue, setFieldTouched } = useFormikContext();
|
const { setFieldValue, setFieldTouched } = useFormikContext();
|
||||||
|
const { me = {} } = useConfig();
|
||||||
const ldapUser = user.ldap_dn;
|
const ldapUser = user.ldap_dn;
|
||||||
const socialAuthUser = user.auth?.length > 0;
|
const socialAuthUser = user.auth?.length > 0;
|
||||||
const externalAccount = user.external_account;
|
const externalAccount = user.external_account;
|
||||||
@@ -119,22 +120,24 @@ function UserFormFields({ user }) {
|
|||||||
validate={required(t`Select a value for this field`)}
|
validate={required(t`Select a value for this field`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FormGroup
|
{me.is_superuser && (
|
||||||
fieldId="user-type"
|
<FormGroup
|
||||||
helperTextInvalid={userTypeMeta.error}
|
fieldId="user-type"
|
||||||
isRequired
|
helperTextInvalid={userTypeMeta.error}
|
||||||
validated={
|
isRequired
|
||||||
!userTypeMeta.touched || !userTypeMeta.error ? 'default' : 'error'
|
validated={
|
||||||
}
|
!userTypeMeta.touched || !userTypeMeta.error ? 'default' : 'error'
|
||||||
label={t`User Type`}
|
}
|
||||||
>
|
label={t`User Type`}
|
||||||
<AnsibleSelect
|
>
|
||||||
isValid={!userTypeMeta.touched || !userTypeMeta.error}
|
<AnsibleSelect
|
||||||
id="user-type"
|
isValid={!userTypeMeta.touched || !userTypeMeta.error}
|
||||||
data={userTypeOptions}
|
id="user-type"
|
||||||
{...userTypeField}
|
data={userTypeOptions}
|
||||||
/>
|
{...userTypeField}
|
||||||
</FormGroup>
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,4 +230,25 @@ describe('<UserForm />', () => {
|
|||||||
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
expect(handleCancel).toBeCalled();
|
expect(handleCancel).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not show user type field', async () => {
|
||||||
|
const handleCancel = jest.fn();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserForm
|
||||||
|
user={mockData}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
config: {
|
||||||
|
me: { is_superuser: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('FormGroup[label="User Type"]')).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Variables which will be made available to jobs ran inside the workflow.
|
- Variables which will be made available to jobs ran inside the workflow.
|
||||||
type: dict
|
type: dict
|
||||||
execution_environment:
|
|
||||||
description:
|
|
||||||
- Execution Environment to use for the WFJT.
|
|
||||||
type: str
|
|
||||||
organization:
|
organization:
|
||||||
description:
|
description:
|
||||||
- Organization the workflow job template exists in.
|
- Organization the workflow job template exists in.
|
||||||
@@ -666,7 +662,6 @@ def main():
|
|||||||
description=dict(),
|
description=dict(),
|
||||||
extra_vars=dict(type='dict'),
|
extra_vars=dict(type='dict'),
|
||||||
organization=dict(),
|
organization=dict(),
|
||||||
execution_environment=dict(),
|
|
||||||
survey_spec=dict(type='dict', aliases=['survey']),
|
survey_spec=dict(type='dict', aliases=['survey']),
|
||||||
survey_enabled=dict(type='bool'),
|
survey_enabled=dict(type='bool'),
|
||||||
allow_simultaneous=dict(type='bool'),
|
allow_simultaneous=dict(type='bool'),
|
||||||
@@ -713,10 +708,6 @@ def main():
|
|||||||
organization_id = module.resolve_name_to_id('organizations', organization)
|
organization_id = module.resolve_name_to_id('organizations', organization)
|
||||||
search_fields['organization'] = new_fields['organization'] = organization_id
|
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
|
# 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})
|
existing_item = module.get_one('workflow_job_templates', name_or_id=name, **{'data': search_fields})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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', ''):
|
if getattr(self, 'job_explanation', ''):
|
||||||
msg += '\njob_explanation: {}'.format(bytes_to_str(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', ''):
|
if getattr(self, 'result_traceback', ''):
|
||||||
msg += '\nresult_traceback:\n{}'.format(bytes_to_str(self.result_traceback))
|
msg += '\nresult_traceback:\n{}'.format(bytes_to_str(self.result_traceback))
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ sdb
|
|||||||
remote-pdb
|
remote-pdb
|
||||||
gprof2dot
|
gprof2dot
|
||||||
atomicwrites==1.4.0
|
atomicwrites==1.4.0
|
||||||
|
flake8
|
||||||
|
yamllint
|
||||||
|
|||||||
Reference in New Issue
Block a user