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