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:
softwarefactory-project-zuul[bot]
2021-06-16 21:44:52 +00:00
committed by GitHub
50 changed files with 847 additions and 319 deletions

View File

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

View File

@@ -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):

View File

@@ -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"])),
) )

View File

@@ -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'

View File

@@ -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:

View File

@@ -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:

View File

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

View File

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

View File

@@ -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>
); );

View File

@@ -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);
}); });

View File

@@ -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;

View File

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

View File

@@ -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);
});
}); });

View File

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

View File

@@ -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);

View File

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

View File

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

View File

@@ -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);
}); });

View File

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

View File

@@ -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(

View File

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

View File

@@ -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);
}); });

View File

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

View File

@@ -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(

View File

@@ -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({

View File

@@ -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;

View 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',
]);
});
});

View File

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

View File

@@ -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'
); );
}); });

View File

@@ -34,6 +34,7 @@ function HostEdit({ host }) {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
handleCancel={handleCancel} handleCancel={handleCancel}
submitError={formError} submitError={formError}
disableInventoryLookup
/> />
</CardBody> </CardBody>
); );

View File

@@ -141,8 +141,10 @@ function InstanceList() {
[instanceGroupId] [instanceGroupId]
); );
const readInstancesOptions = () => const readInstancesOptions = useCallback(
InstanceGroupsAPI.readInstanceOptions(instanceGroupId); () => InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
[instanceGroupId]
);
return ( return (
<> <>

View File

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

View File

@@ -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());

View File

@@ -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) {

View File

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

View File

@@ -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">

View File

@@ -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');

View File

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

View File

@@ -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 && (

View File

@@ -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', () => {

View File

@@ -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>
</> </>
); );

View File

@@ -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);
}); });
}); });

View File

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

View File

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

View File

@@ -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",

View File

@@ -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>
)}
</> </>
); );
} }

View File

@@ -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);
});
}); });

View File

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

View File

@@ -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))

View File

@@ -21,3 +21,5 @@ sdb
remote-pdb remote-pdb
gprof2dot gprof2dot
atomicwrites==1.4.0 atomicwrites==1.4.0
flake8
yamllint