mirror of
https://github.com/ansible/awx.git
synced 2026-03-10 22:19:28 -02:30
Merge pull request #7975 from AlexSCorey/7973-DisableTemplateFormFields
Disables template and workflow template fields. Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -23,6 +23,7 @@ function FieldWithPrompt({
|
|||||||
promptId,
|
promptId,
|
||||||
promptName,
|
promptName,
|
||||||
tooltip,
|
tooltip,
|
||||||
|
isDisabled,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="pf-c-form__group">
|
<div className="pf-c-form__group">
|
||||||
@@ -39,6 +40,7 @@ function FieldWithPrompt({
|
|||||||
{tooltip && <FieldTooltip content={tooltip} />}
|
{tooltip && <FieldTooltip content={tooltip} />}
|
||||||
</div>
|
</div>
|
||||||
<StyledCheckboxField
|
<StyledCheckboxField
|
||||||
|
isDisabled={isDisabled}
|
||||||
id={promptId}
|
id={promptId}
|
||||||
label={i18n._(t`Prompt on launch`)}
|
label={i18n._(t`Prompt on launch`)}
|
||||||
name={promptName}
|
name={promptName}
|
||||||
|
|||||||
@@ -9,10 +9,19 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function CheckboxField({ id, name, label, tooltip, validate, ...rest }) {
|
function CheckboxField({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
validate,
|
||||||
|
isDisabled,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
const [field] = useField({ name, validate });
|
const [field] = useField({ name, validate });
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
isDisabled={isDisabled}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function CredentialLookup({
|
|||||||
history,
|
history,
|
||||||
i18n,
|
i18n,
|
||||||
tooltip,
|
tooltip,
|
||||||
|
isDisabled,
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
result: { count, credentials, relatedSearchableKeys, searchableKeys },
|
result: { count, credentials, relatedSearchableKeys, searchableKeys },
|
||||||
@@ -108,6 +109,7 @@ function CredentialLookup({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
required={required}
|
required={required}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
|
isDisabled={isDisabled}
|
||||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
<OptionsList
|
<OptionsList
|
||||||
value={state.selectedItems}
|
value={state.selectedItems}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import OptionsList from '../OptionsList';
|
|||||||
import useRequest from '../../util/useRequest';
|
import useRequest from '../../util/useRequest';
|
||||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||||
|
import FieldWithPrompt from '../FieldWithPrompt';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('inventory', {
|
const QS_CONFIG = getQSConfig('inventory', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -17,9 +18,26 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
function InventoryLookup({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
i18n,
|
||||||
|
history,
|
||||||
|
required,
|
||||||
|
isPromptableField,
|
||||||
|
fieldId,
|
||||||
|
promptId,
|
||||||
|
promptName,
|
||||||
|
}) {
|
||||||
const {
|
const {
|
||||||
result: { inventories, count, relatedSearchableKeys, searchableKeys },
|
result: {
|
||||||
|
inventories,
|
||||||
|
count,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
searchableKeys,
|
||||||
|
canEdit,
|
||||||
|
},
|
||||||
request: fetchInventories,
|
request: fetchInventories,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -39,16 +57,86 @@ function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
actionsResponse.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
|
canEdit: Boolean(actionsResponse.data.actions.POST),
|
||||||
};
|
};
|
||||||
}, [history.location]),
|
}, [history.location]),
|
||||||
{ inventories: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] }
|
{
|
||||||
|
inventories: [],
|
||||||
|
count: 0,
|
||||||
|
relatedSearchableKeys: [],
|
||||||
|
searchableKeys: [],
|
||||||
|
canEdit: false,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInventories();
|
fetchInventories();
|
||||||
}, [fetchInventories]);
|
}, [fetchInventories]);
|
||||||
|
|
||||||
return (
|
return isPromptableField ? (
|
||||||
|
<>
|
||||||
|
<FieldWithPrompt
|
||||||
|
fieldId={fieldId}
|
||||||
|
isRequired={required}
|
||||||
|
label={i18n._(t`Inventory`)}
|
||||||
|
promptId={promptId}
|
||||||
|
promptName={promptName}
|
||||||
|
isDisabled={!canEdit}
|
||||||
|
tooltip={i18n._(t`Select the inventory containing the hosts
|
||||||
|
you want this job to manage.`)}
|
||||||
|
>
|
||||||
|
<Lookup
|
||||||
|
id="inventory-lookup"
|
||||||
|
header={i18n._(t`Inventory`)}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
required={required}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={!canEdit}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
|
<OptionsList
|
||||||
|
value={state.selectedItems}
|
||||||
|
options={inventories}
|
||||||
|
optionCount={count}
|
||||||
|
searchColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name__icontains',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Created By (Username)`),
|
||||||
|
key: 'created_by__username__icontains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Modified By (Username)`),
|
||||||
|
key: 'modified_by__username__icontains',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
sortColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
|
multiple={state.multiple}
|
||||||
|
header={i18n._(t`Inventory`)}
|
||||||
|
name="inventory"
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
readOnly={!canDelete}
|
||||||
|
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||||
|
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<LookupErrorMessage error={error} />
|
||||||
|
</FieldWithPrompt>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<Lookup
|
<Lookup
|
||||||
id="inventory-lookup"
|
id="inventory-lookup"
|
||||||
@@ -58,6 +146,7 @@ function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
required={required}
|
required={required}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
isDisabled={!canEdit}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
<OptionsList
|
<OptionsList
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import { QSConfig } from '../../types';
|
|||||||
|
|
||||||
const ChipHolder = styled.div`
|
const ChipHolder = styled.div`
|
||||||
--pf-c-form-control--Height: auto;
|
--pf-c-form-control--Height: auto;
|
||||||
|
background-color: ${props =>
|
||||||
|
props.isDisabled ? 'var(--pf-global--disabled-color--300)' : null};
|
||||||
`;
|
`;
|
||||||
function Lookup(props) {
|
function Lookup(props) {
|
||||||
const {
|
const {
|
||||||
@@ -43,6 +45,7 @@ function Lookup(props) {
|
|||||||
renderOptionsList,
|
renderOptionsList,
|
||||||
history,
|
history,
|
||||||
i18n,
|
i18n,
|
||||||
|
isDisabled,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(
|
const [state, dispatch] = useReducer(
|
||||||
@@ -88,7 +91,8 @@ function Lookup(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { isModalOpen, selectedItems } = state;
|
const { isModalOpen, selectedItems } = state;
|
||||||
const canDelete = !required || (multiple && value.length > 1);
|
const canDelete =
|
||||||
|
(!required || (multiple && value.length > 1)) && !isDisabled;
|
||||||
let items = [];
|
let items = [];
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
items = value;
|
items = value;
|
||||||
@@ -103,11 +107,11 @@ function Lookup(props) {
|
|||||||
id={id}
|
id={id}
|
||||||
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
|
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
|
||||||
variant={ButtonVariant.control}
|
variant={ButtonVariant.control}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading || isDisabled}
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ChipHolder className="pf-c-form-control">
|
<ChipHolder isDisabled={isDisabled} className="pf-c-form-control">
|
||||||
<ChipGroup numChips={5} totalChips={items.length}>
|
<ChipGroup numChips={5} totalChips={items.length}>
|
||||||
{items.map(item =>
|
{items.map(item =>
|
||||||
renderItemChip({
|
renderItemChip({
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function ProjectLookup({
|
|||||||
history,
|
history,
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
result: { projects, count, relatedSearchableKeys, searchableKeys },
|
result: { projects, count, relatedSearchableKeys, searchableKeys, canEdit },
|
||||||
request: fetchProjects,
|
request: fetchProjects,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -55,6 +55,7 @@ function ProjectLookup({
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
actionsResponse.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
|
canEdit: Boolean(actionsResponse.data.actions.POST),
|
||||||
};
|
};
|
||||||
}, [history.location.search, autocomplete]),
|
}, [history.location.search, autocomplete]),
|
||||||
{
|
{
|
||||||
@@ -62,6 +63,7 @@ function ProjectLookup({
|
|||||||
projects: [],
|
projects: [],
|
||||||
relatedSearchableKeys: [],
|
relatedSearchableKeys: [],
|
||||||
searchableKeys: [],
|
searchableKeys: [],
|
||||||
|
canEdit: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,6 +89,7 @@ function ProjectLookup({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
required={required}
|
required={required}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
isDisabled={!canEdit}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
<OptionsList
|
<OptionsList
|
||||||
|
|||||||
@@ -11,12 +11,22 @@ export default function useSyncedSelectValue(value, onChange) {
|
|||||||
const [selections, setSelections] = useState([]);
|
const [selections, setSelections] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const newOptions = [];
|
||||||
if (value !== selections && options.length) {
|
if (value !== selections && options.length) {
|
||||||
const syncedValue = value.map(item =>
|
const syncedValue = value.map(item => {
|
||||||
options.find(i => i.id === item.id)
|
const match = options.find(i => {
|
||||||
);
|
return i.id === item.id;
|
||||||
|
});
|
||||||
|
if (!match) {
|
||||||
|
newOptions.push(item);
|
||||||
|
}
|
||||||
|
return match || item;
|
||||||
|
});
|
||||||
setSelections(syncedValue);
|
setSelections(syncedValue);
|
||||||
}
|
}
|
||||||
|
if (newOptions.length > 0) {
|
||||||
|
setOptions(options.concat(newOptions));
|
||||||
|
}
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
}, [value, options]);
|
}, [value, options]);
|
||||||
|
|
||||||
@@ -27,7 +37,6 @@ export default function useSyncedSelectValue(value, onChange) {
|
|||||||
onChange(selections.concat(item));
|
onChange(selections.concat(item));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selections: options.length ? addToStringToObjects(selections) : [],
|
selections: options.length ? addToStringToObjects(selections) : [],
|
||||||
onSelect,
|
onSelect,
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import { withRouter, Redirect } from 'react-router-dom';
|
|||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
import ContentLoading from '../../../components/ContentLoading';
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
|
import { JobTemplatesAPI } from '../../../api';
|
||||||
import { JobTemplate } from '../../../types';
|
import { JobTemplate } from '../../../types';
|
||||||
import { getAddedAndRemoved } from '../../../util/lists';
|
import { getAddedAndRemoved } from '../../../util/lists';
|
||||||
import JobTemplateForm from '../shared/JobTemplateForm';
|
import JobTemplateForm from '../shared/JobTemplateForm';
|
||||||
|
|
||||||
const loadRelatedProjectPlaybooks = async project =>
|
|
||||||
ProjectsAPI.readPlaybooks(project);
|
|
||||||
class JobTemplateEdit extends Component {
|
class JobTemplateEdit extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
template: JobTemplate.isRequired,
|
template: JobTemplate.isRequired,
|
||||||
@@ -43,17 +41,8 @@ class JobTemplateEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadRelated() {
|
async loadRelated() {
|
||||||
const {
|
|
||||||
template: { project },
|
|
||||||
} = this.props;
|
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
if (project) {
|
|
||||||
const { data: playbook = [] } = await loadRelatedProjectPlaybooks(
|
|
||||||
project
|
|
||||||
);
|
|
||||||
this.setState({ relatedProjectPlaybooks: playbook });
|
|
||||||
}
|
|
||||||
const [relatedCredentials] = await this.loadRelatedCredentials();
|
const [relatedCredentials] = await this.loadRelatedCredentials();
|
||||||
this.setState({
|
this.setState({
|
||||||
relatedCredentials,
|
relatedCredentials,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
JobTemplatesAPI,
|
JobTemplatesAPI,
|
||||||
LabelsAPI,
|
LabelsAPI,
|
||||||
ProjectsAPI,
|
ProjectsAPI,
|
||||||
|
InventoriesAPI,
|
||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
import JobTemplateEdit from './JobTemplateEdit';
|
import JobTemplateEdit from './JobTemplateEdit';
|
||||||
|
|
||||||
@@ -181,6 +182,12 @@ JobTemplatesAPI.readCredentials.mockResolvedValue({
|
|||||||
ProjectsAPI.readPlaybooks.mockResolvedValue({
|
ProjectsAPI.readPlaybooks.mockResolvedValue({
|
||||||
data: mockRelatedProjectPlaybooks,
|
data: mockRelatedProjectPlaybooks,
|
||||||
});
|
});
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {}, POST: {} } },
|
||||||
|
});
|
||||||
|
ProjectsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {}, POST: {} } },
|
||||||
|
});
|
||||||
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
ProjectLookup,
|
ProjectLookup,
|
||||||
MultiCredentialsLookup,
|
MultiCredentialsLookup,
|
||||||
} from '../../../components/Lookup';
|
} from '../../../components/Lookup';
|
||||||
import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
|
import { JobTemplatesAPI } from '../../../api';
|
||||||
import LabelSelect from './LabelSelect';
|
import LabelSelect from './LabelSelect';
|
||||||
import PlaybookSelect from './PlaybookSelect';
|
import PlaybookSelect from './PlaybookSelect';
|
||||||
import WebhookSubForm from './WebhookSubForm';
|
import WebhookSubForm from './WebhookSubForm';
|
||||||
@@ -100,18 +100,6 @@ function JobTemplateForm({
|
|||||||
'webhook_credential'
|
'webhook_credential'
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
request: fetchProject,
|
|
||||||
error: projectContentError,
|
|
||||||
contentLoading: hasProjectLoading,
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
|
||||||
if (template?.project) {
|
|
||||||
await ProjectsAPI.readDetail(template?.project);
|
|
||||||
}
|
|
||||||
}, [template])
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
request: loadRelatedInstanceGroups,
|
request: loadRelatedInstanceGroups,
|
||||||
error: instanceGroupError,
|
error: instanceGroupError,
|
||||||
@@ -127,10 +115,6 @@ function JobTemplateForm({
|
|||||||
}, [setFieldValue, template])
|
}, [setFieldValue, template])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchProject();
|
|
||||||
}, [fetchProject]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRelatedInstanceGroups();
|
loadRelatedInstanceGroups();
|
||||||
}, [loadRelatedInstanceGroups]);
|
}, [loadRelatedInstanceGroups]);
|
||||||
@@ -204,16 +188,12 @@ function JobTemplateForm({
|
|||||||
callbackUrl = `${origin}${path}`;
|
callbackUrl = `${origin}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceGroupLoading || hasProjectLoading) {
|
if (instanceGroupLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentError || instanceGroupError || projectContentError) {
|
if (contentError || instanceGroupError) {
|
||||||
return (
|
return <ContentError error={contentError || instanceGroupError} />;
|
||||||
<ContentError
|
|
||||||
error={contentError || instanceGroupError || projectContentError}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -254,17 +234,15 @@ function JobTemplateForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FieldWithPrompt>
|
</FieldWithPrompt>
|
||||||
<FieldWithPrompt
|
<>
|
||||||
fieldId="template-inventory"
|
|
||||||
isRequired={!askInventoryOnLaunchField.value}
|
|
||||||
label={i18n._(t`Inventory`)}
|
|
||||||
promptId="template-ask-inventory-on-launch"
|
|
||||||
promptName="ask_inventory_on_launch"
|
|
||||||
tooltip={i18n._(t`Select the inventory containing the hosts
|
|
||||||
you want this job to manage.`)}
|
|
||||||
>
|
|
||||||
<InventoryLookup
|
<InventoryLookup
|
||||||
value={inventory}
|
value={inventory}
|
||||||
|
fieldId="template-inventory"
|
||||||
|
promptId="template-ask-inventory-on-launch"
|
||||||
|
promptName="ask_inventory_on_launch"
|
||||||
|
isPromptableField
|
||||||
|
tooltip={i18n._(t`Select the inventory containing the hosts
|
||||||
|
you want this job to manage.`)}
|
||||||
onBlur={() => inventoryHelpers.setTouched()}
|
onBlur={() => inventoryHelpers.setTouched()}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
inventoryHelpers.setValue(value ? value.id : null);
|
inventoryHelpers.setValue(value ? value.id : null);
|
||||||
@@ -283,7 +261,7 @@ function JobTemplateForm({
|
|||||||
{inventoryMeta.error}
|
{inventoryMeta.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FieldWithPrompt>
|
</>
|
||||||
<ProjectLookup
|
<ProjectLookup
|
||||||
value={projectField.value}
|
value={projectField.value}
|
||||||
onBlur={() => projectHelpers.setTouched()}
|
onBlur={() => projectHelpers.setTouched()}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ProjectsAPI,
|
ProjectsAPI,
|
||||||
CredentialsAPI,
|
CredentialsAPI,
|
||||||
CredentialTypesAPI,
|
CredentialTypesAPI,
|
||||||
|
InventoriesAPI,
|
||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -111,14 +112,23 @@ describe('<JobTemplateForm />', () => {
|
|||||||
JobTemplatesAPI.updateWebhookKey.mockReturnValue({
|
JobTemplatesAPI.updateWebhookKey.mockReturnValue({
|
||||||
data: { webhook_key: 'webhook key' },
|
data: { webhook_key: 'webhook key' },
|
||||||
});
|
});
|
||||||
ProjectsAPI.readPlaybooks.mockReturnValue({
|
JobTemplatesAPI.updateWebhookKey.mockReturnValue({
|
||||||
data: ['debug.yml'],
|
data: { webhook_key: 'webhook key' },
|
||||||
});
|
});
|
||||||
ProjectsAPI.readDetail.mockReturnValue({
|
ProjectsAPI.readDetail.mockReturnValue({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 1,
|
id: 1,
|
||||||
allow_override: true,
|
allow_override: true,
|
||||||
});
|
});
|
||||||
|
ProjectsAPI.readPlaybooks.mockReturnValue({
|
||||||
|
data: ['debug.yml'],
|
||||||
|
});
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {}, POST: {} } },
|
||||||
|
});
|
||||||
|
ProjectsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {}, POST: {} } },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { number, string, oneOfType } from 'prop-types';
|
import { number, string, oneOfType } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -7,6 +7,7 @@ import { ProjectsAPI } from '../../../api';
|
|||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
|
|
||||||
function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
|
function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
|
||||||
|
const [isDisabled, setIsDisabled] = useState(false);
|
||||||
const {
|
const {
|
||||||
result: options,
|
result: options,
|
||||||
request: fetchOptions,
|
request: fetchOptions,
|
||||||
@@ -18,6 +19,7 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { data } = await ProjectsAPI.readPlaybooks(projectId);
|
const { data } = await ProjectsAPI.readPlaybooks(projectId);
|
||||||
|
|
||||||
const opts = (data || []).map(playbook => ({
|
const opts = (data || []).map(playbook => ({
|
||||||
value: playbook,
|
value: playbook,
|
||||||
key: playbook,
|
key: playbook,
|
||||||
@@ -42,18 +44,30 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
onError(error);
|
if (error.response.status === 403) {
|
||||||
|
setIsDisabled(true);
|
||||||
|
} else {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [error, onError]);
|
}, [error, onError]);
|
||||||
|
|
||||||
|
const isDisabledData = [
|
||||||
|
{
|
||||||
|
value: field.value || '',
|
||||||
|
label: field.value || '',
|
||||||
|
key: 1,
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
id="template-playbook"
|
id="template-playbook"
|
||||||
data={options}
|
data={isDisabled ? isDisabledData : options}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
{...field}
|
{...field}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading || isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,23 +110,21 @@ function WorkflowJobTemplateForm({
|
|||||||
value={organizationField.value}
|
value={organizationField.value}
|
||||||
isValid={!organizationMeta.error}
|
isValid={!organizationMeta.error}
|
||||||
/>
|
/>
|
||||||
|
<>
|
||||||
<FieldWithPrompt
|
|
||||||
fieldId="wfjt-inventory"
|
|
||||||
label={i18n._(t`Inventory`)}
|
|
||||||
promptId="wfjt-ask-inventory-on-launch"
|
|
||||||
promptName="ask_inventory_on_launch"
|
|
||||||
tooltip={i18n._(
|
|
||||||
t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<InventoryLookup
|
<InventoryLookup
|
||||||
|
promptId="wfjt-ask-inventory-on-launch"
|
||||||
|
promptName="ask_inventory_on_launch"
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.`
|
||||||
|
)}
|
||||||
|
fieldId="wfjt-inventory"
|
||||||
|
isPromptableField
|
||||||
value={inventoryField.value}
|
value={inventoryField.value}
|
||||||
onBlur={() => inventoryHelpers.setTouched()}
|
onBlur={() => inventoryHelpers.setTouched()}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
inventoryHelpers.setValue(value);
|
inventoryHelpers.setValue(value);
|
||||||
}}
|
}}
|
||||||
required={askInventoryOnLaunchField.value}
|
required={!askInventoryOnLaunchField.value}
|
||||||
touched={inventoryMeta.touched}
|
touched={inventoryMeta.touched}
|
||||||
error={inventoryMeta.error}
|
error={inventoryMeta.error}
|
||||||
/>
|
/>
|
||||||
@@ -139,8 +137,7 @@ function WorkflowJobTemplateForm({
|
|||||||
{inventoryMeta.error}
|
{inventoryMeta.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FieldWithPrompt>
|
</>
|
||||||
|
|
||||||
<FieldWithPrompt
|
<FieldWithPrompt
|
||||||
fieldId="wjft-limit"
|
fieldId="wjft-limit"
|
||||||
label={i18n._(t`Limit`)}
|
label={i18n._(t`Limit`)}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
LabelsAPI,
|
LabelsAPI,
|
||||||
OrganizationsAPI,
|
OrganizationsAPI,
|
||||||
InventoriesAPI,
|
InventoriesAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
|
|
||||||
jest.mock('../../../api/models/CredentialTypes');
|
jest.mock('../../../api/models/CredentialTypes');
|
||||||
@@ -18,6 +20,8 @@ jest.mock('../../../api/models/WorkflowJobTemplates');
|
|||||||
jest.mock('../../../api/models/Labels');
|
jest.mock('../../../api/models/Labels');
|
||||||
jest.mock('../../../api/models/Organizations');
|
jest.mock('../../../api/models/Organizations');
|
||||||
jest.mock('../../../api/models/Inventories');
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
jest.mock('../../../api/models/Projects');
|
||||||
|
jest.mock('../../../api/models/Credentials');
|
||||||
|
|
||||||
describe('<WorkflowJobTemplateForm/>', () => {
|
describe('<WorkflowJobTemplateForm/>', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
@@ -71,6 +75,15 @@ describe('<WorkflowJobTemplateForm/>', () => {
|
|||||||
{ id: 2, name: 'Bar' },
|
{ id: 2, name: 'Bar' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
CredentialTypesAPI.read.mockResolvedValue({
|
||||||
|
data: { results: [{ id: 1 }] },
|
||||||
|
});
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {}, POST: {} } },
|
||||||
|
});
|
||||||
|
ProjectsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {}, POST: {} } },
|
||||||
|
});
|
||||||
|
|
||||||
history = createMemoryHistory({
|
history = createMemoryHistory({
|
||||||
initialEntries: ['/templates/workflow_job_template/6/edit'],
|
initialEntries: ['/templates/workflow_job_template/6/edit'],
|
||||||
|
|||||||
Reference in New Issue
Block a user