diff --git a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx index 3abb85604e..102abc4d60 100644 --- a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx @@ -19,7 +19,13 @@ const QS_CONFIG = getQSConfig('inventory', { function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) { const { - result: { inventories, count, relatedSearchableKeys, searchableKeys }, + result: { + inventories, + count, + relatedSearchableKeys, + searchableKeys, + canEdit, + }, request: fetchInventories, error, isLoading, @@ -39,9 +45,16 @@ function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) { searchableKeys: Object.keys( actionsResponse.data.actions?.GET || {} ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + canEdit: Boolean(actionsResponse.data.actions.POST), }; }, [history.location]), - { inventories: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } + { + inventories: [], + count: 0, + relatedSearchableKeys: [], + searchableKeys: [], + canEdit: false, + } ); useEffect(() => { @@ -58,6 +71,7 @@ function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) { onBlur={onBlur} required={required} isLoading={isLoading} + isDisabled={!canEdit} qsConfig={QS_CONFIG} renderOptionsList={({ state, dispatch, canDelete }) => ( + props.isDisabled ? 'var(--pf-global--disabled-color--300)' : null}; `; function Lookup(props) { const { @@ -43,6 +45,7 @@ function Lookup(props) { renderOptionsList, history, i18n, + isDisabled, } = props; const [state, dispatch] = useReducer( @@ -103,11 +106,15 @@ function Lookup(props) { id={id} onClick={() => dispatch({ type: 'TOGGLE_MODAL' })} variant={ButtonVariant.control} - isDisabled={isLoading} + isDisabled={isLoading || isDisabled} > - + {items.map(item => renderItemChip({ diff --git a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx index 5c8ec16dee..3858c6a7fb 100644 --- a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx @@ -32,7 +32,7 @@ function ProjectLookup({ history, }) { const { - result: { projects, count, relatedSearchableKeys, searchableKeys }, + result: { projects, count, relatedSearchableKeys, searchableKeys, canEdit }, request: fetchProjects, error, isLoading, @@ -55,6 +55,7 @@ function ProjectLookup({ searchableKeys: Object.keys( actionsResponse.data.actions?.GET || {} ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + canEdit: Boolean(actionsResponse.data.actions.POST), }; }, [history.location.search, autocomplete]), { @@ -62,6 +63,7 @@ function ProjectLookup({ projects: [], relatedSearchableKeys: [], searchableKeys: [], + canEdit: false, } ); @@ -87,6 +89,7 @@ function ProjectLookup({ onChange={onChange} required={required} isLoading={isLoading} + isDisabled={!canEdit} qsConfig={QS_CONFIG} renderOptionsList={({ state, dispatch, canDelete }) => ( { + const newOptions = []; if (value !== selections && options.length) { - const syncedValue = value.map(item => - options.find(i => i.id === item.id) - ); + const syncedValue = value.map(item => { + const match = options.find(i => { + return i.id === item.id; + }); + if (!match) { + newOptions.push(item); + } + return match || item; + }); setSelections(syncedValue); } + if (newOptions.length > 0) { + setOptions(options.concat(newOptions)); + } /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [value, options]); @@ -27,7 +37,6 @@ export default function useSyncedSelectValue(value, onChange) { onChange(selections.concat(item)); } }; - return { selections: options.length ? addToStringToObjects(selections) : [], onSelect, diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 1e48523d85..a51bbb355a 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -4,13 +4,11 @@ import { withRouter, Redirect } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; import ContentError from '../../../components/ContentError'; import ContentLoading from '../../../components/ContentLoading'; -import { JobTemplatesAPI, ProjectsAPI } from '../../../api'; +import { JobTemplatesAPI } from '../../../api'; import { JobTemplate } from '../../../types'; import { getAddedAndRemoved } from '../../../util/lists'; import JobTemplateForm from '../shared/JobTemplateForm'; -const loadRelatedProjectPlaybooks = async project => - ProjectsAPI.readPlaybooks(project); class JobTemplateEdit extends Component { static propTypes = { template: JobTemplate.isRequired, @@ -43,17 +41,8 @@ class JobTemplateEdit extends Component { } async loadRelated() { - const { - template: { project }, - } = this.props; this.setState({ contentError: null, hasContentLoading: true }); try { - if (project) { - const { data: playbook = [] } = await loadRelatedProjectPlaybooks( - project - ); - this.setState({ relatedProjectPlaybooks: playbook }); - } const [relatedCredentials] = await this.loadRelatedCredentials(); this.setState({ relatedCredentials, diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index a7d540a7a7..c75efc036f 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -39,7 +39,7 @@ import { ProjectLookup, MultiCredentialsLookup, } from '../../../components/Lookup'; -import { JobTemplatesAPI, ProjectsAPI } from '../../../api'; +import { JobTemplatesAPI } from '../../../api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; import WebhookSubForm from './WebhookSubForm'; @@ -100,18 +100,6 @@ function JobTemplateForm({ 'webhook_credential' ); - const { - request: fetchProject, - error: projectContentError, - contentLoading: hasProjectLoading, - } = useRequest( - useCallback(async () => { - if (template?.project) { - await ProjectsAPI.readDetail(template?.project); - } - }, [template]) - ); - const { request: loadRelatedInstanceGroups, error: instanceGroupError, @@ -127,10 +115,6 @@ function JobTemplateForm({ }, [setFieldValue, template]) ); - useEffect(() => { - fetchProject(); - }, [fetchProject]); - useEffect(() => { loadRelatedInstanceGroups(); }, [loadRelatedInstanceGroups]); @@ -204,16 +188,12 @@ function JobTemplateForm({ callbackUrl = `${origin}${path}`; } - if (instanceGroupLoading || hasProjectLoading) { + if (instanceGroupLoading) { return ; } - if (contentError || instanceGroupError || projectContentError) { - return ( - - ); + if (contentError || instanceGroupError) { + return ; } return ( diff --git a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx index 78fd6e28f9..83dfc590af 100644 --- a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx +++ b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx @@ -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 { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -7,6 +7,7 @@ import { ProjectsAPI } from '../../../api'; import useRequest from '../../../util/useRequest'; function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) { + const [isDisabled, setIsDisabled] = useState(false); const { result: options, request: fetchOptions, @@ -18,6 +19,7 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) { return []; } const { data } = await ProjectsAPI.readPlaybooks(projectId); + const opts = (data || []).map(playbook => ({ value: playbook, key: playbook, @@ -33,7 +35,7 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) { }); return opts; }, [projectId, i18n]), - [] + [field.value] ); useEffect(() => { @@ -42,18 +44,30 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) { useEffect(() => { if (error) { - onError(error); + if (error.response.status === 403) { + setIsDisabled(true); + } else { + onError(error); + } } }, [error, onError]); + const isDisabledData = [ + { + value: field.value || '', + label: field.value || '', + key: 1, + isDisabled: true, + }, + ]; return ( ); }