diff --git a/awx/ui_next/src/App.jsx b/awx/ui_next/src/App.jsx index e88291fc7c..704515c113 100644 --- a/awx/ui_next/src/App.jsx +++ b/awx/ui_next/src/App.jsx @@ -97,7 +97,13 @@ class App extends Component { MeAPI.read(), ]); const { - data: { ansible_version, custom_virtualenvs, version }, + data: { + ansible_version, + custom_virtualenvs, + project_base_dir, + project_local_paths, + version, + }, } = configRes; const { data: { @@ -105,7 +111,14 @@ class App extends Component { }, } = meRes; - this.setState({ ansible_version, custom_virtualenvs, version, me }); + this.setState({ + ansible_version, + custom_virtualenvs, + project_base_dir, + project_local_paths, + version, + me, + }); } catch (err) { this.setState({ configError: err }); } @@ -115,6 +128,8 @@ class App extends Component { const { ansible_version, custom_virtualenvs, + project_base_dir, + project_local_paths, isAboutModalOpen, isNavOpen, me, @@ -169,7 +184,14 @@ class App extends Component { {render({ routeGroups })} diff --git a/awx/ui_next/src/app.scss b/awx/ui_next/src/app.scss index cf8f2d2338..4498268a13 100644 --- a/awx/ui_next/src/app.scss +++ b/awx/ui_next/src/app.scss @@ -189,10 +189,6 @@ z-index: 20; } -.pf-c-alert__icon { - --pf-c-alert__icon--Color: white; -} - .at-u-textRight { text-align: right; } diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx index 526d918629..5eeb4eeb45 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx @@ -23,6 +23,9 @@ function ProjectAdd({ history, i18n }) { const [formSubmitError, setFormSubmitError] = useState(null); const handleSubmit = async values => { + if (values.scm_type === 'manual') { + values.scm_type = ''; + } setFormSubmitError(null); try { const { diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx index 3bd0469dea..de8840e911 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx @@ -16,6 +16,7 @@ describe('', () => { scm_url: 'https://foo.bar', scm_clean: true, credential: 100, + local_path: '', organization: 2, scm_update_on_launch: true, scm_update_cache_timeout: 3, @@ -116,9 +117,15 @@ describe('', () => { }); test('handleSubmit should throw an error', async () => { + const config = { + project_local_paths: ['foobar', 'qux'], + project_base_dir: 'dir/foo/bar', + }; ProjectsAPI.create.mockImplementation(() => Promise.reject(new Error())); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts(, { + context: { config }, + }); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); const formik = wrapper.find('Formik').instance(); @@ -127,6 +134,7 @@ describe('', () => { { values: { ...projectData, + scm_type: 'manual', }, }, () => resolve() diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index ab738c97b3..1c391f9b9f 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -5,9 +5,11 @@ import { t } from '@lingui/macro'; import styled from 'styled-components'; import { Project } from '@types'; import { formatDateString } from '@util/dates'; +import { Config } from '@contexts/Config'; import { Button, CardBody, List, ListItem } from '@patternfly/react-core'; import { DetailList, Detail } from '@components/DetailList'; import { CredentialChip } from '@components/Chip'; +import { toTitleCase } from '@util/strings'; const ActionButtonWrapper = styled.div` display: flex; @@ -25,6 +27,7 @@ function ProjectDetail({ project, i18n }) { custom_virtualenv, description, id, + local_path, modified, name, scm_branch, @@ -93,10 +96,21 @@ function ProjectDetail({ project, i18n }) { {summary_fields.organization && ( + {summary_fields.organization.name} + + } /> )} - + @@ -123,6 +137,15 @@ function ProjectDetail({ project, i18n }) { label={i18n._(t`Ansible Environment`)} value={custom_virtualenv} /> + + {({ project_base_dir }) => ( + + )} + + {/* TODO: Link to user in users */} {/* TODO: Link to user in users */} diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx index 1fb9b9c533..ef62183a9f 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx @@ -85,7 +85,7 @@ describe('', () => { assertDetail('Name', mockProject.name); assertDetail('Description', mockProject.description); assertDetail('Organization', mockProject.summary_fields.organization.name); - assertDetail('SCM Type', mockProject.scm_type); + assertDetail('SCM Type', 'Git'); assertDetail('SCM URL', mockProject.scm_url); assertDetail('SCM Branch', mockProject.scm_branch); assertDetail('SCM Refspec', mockProject.scm_refspec); diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx index 9e8f9f67c4..f5d21d8fc2 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx @@ -22,6 +22,9 @@ function ProjectEdit({ project, history, i18n }) { const [formSubmitError, setFormSubmitError] = useState(null); const handleSubmit = async values => { + if (values.scm_type === 'manual') { + values.scm_type = ''; + } try { const { data: { id }, diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx index 99975a807b..8f8b400cf1 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx @@ -17,6 +17,7 @@ describe('', () => { scm_url: 'https://foo.bar', scm_clean: true, credential: 100, + local_path: '', organization: 2, scm_update_on_launch: true, scm_update_cache_timeout: 3, @@ -115,9 +116,18 @@ describe('', () => { }); test('handleSubmit should throw an error', async () => { + const config = { + project_local_paths: [], + project_base_dir: 'foo/bar', + }; ProjectsAPI.update.mockImplementation(() => Promise.reject(new Error())); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + , + { + context: { config }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await act(async () => { diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index 965c5d33af..628167d868 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -19,6 +19,7 @@ import ListActionButton from '@components/ListActionButton'; import ProjectSyncButton from '../shared/ProjectSyncButton'; import { StatusIcon } from '@components/Sparkline'; import VerticalSeparator from '@components/VerticalSeparator'; +import { toTitleCase } from '@util/strings'; import { Project } from '@types'; class ProjectListItem extends React.Component { @@ -97,7 +98,9 @@ class ProjectListItem extends React.Component { , - {project.scm_type.toUpperCase()} + {project.scm_type === '' + ? i18n._(t`Manual`) + : toTitleCase(project.scm_type)} , {project.scm_revision.substring(0, 7)} diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index e1757f4002..5ac2454319 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -22,6 +22,7 @@ import { SvnSubForm, InsightsSubForm, SubFormTitle, + ManualSubForm, } from './ProjectSubForms'; const ScmTypeFormRow = styled(FormRow)` @@ -173,199 +174,224 @@ function ProjectForm({ project, ...props }) { } return ( - ( -
- - - - ( - form.setFieldTouched('organization')} - onChange={value => { - form.setFieldValue('organization', value.id); - setOrganization(value); - }} - value={organization} - required - /> - )} - /> - ( - + {({ project_base_dir, project_local_paths }) => ( + ( + + + - + + ( + form.setFieldTouched('organization')} + onChange={value => { + form.setFieldValue('organization', value.id); + setOrganization(value); + }} + value={organization} + required + /> + )} + /> + ( + + { + if (label === 'Manual') { + value = 'manual'; + } + return { + label, + value, + key: value, + }; + }), + ]} + onChange={(event, value) => { + form.setFieldValue('scm_type', value); + resetScmTypeFields(value, form); + }} + /> + + )} + /> + {formik.values.scm_type !== '' && ( + + + {i18n._(t`Type Details`)} + + { { - value: '', - key: '', - label: i18n._(t`Choose an SCM Type`), - isDisabled: true, - }, - ...scmTypeOptions.map(([value, label]) => { - if (label === 'Manual') { - value = 'manual'; - } - return { - label, - value, - key: value, - }; - }), - ]} - onChange={(event, value) => { - form.setFieldValue('scm_type', value); - resetScmTypeFields(value, form); - }} - /> - - )} - /> - {formik.values.scm_type !== '' && ( - - {i18n._(t`Type Details`)} - { - { - git: ( - + ), + git: ( + + ), + hg: ( + + ), + svn: ( + + ), + insights: ( + + ), + }[formik.values.scm_type] + } + + )} + + {({ custom_virtualenvs }) => + custom_virtualenvs && + custom_virtualenvs.length > 1 && ( + ( + + + datum !== '/venv/ansible/') + .map(datum => ({ + label: datum, + value: datum, + key: datum, + })), + ]} + {...field} + /> + + )} /> - ), - hg: ( - - ), - svn: ( - - ), - insights: ( - - ), - }[formik.values.scm_type] - } - - )} - - {({ custom_virtualenvs }) => - custom_virtualenvs && - custom_virtualenvs.length > 1 && ( - ( - - - datum !== '/venv/ansible/') - .map(datum => ({ - label: datum, - value: datum, - key: datum, - })), - ]} - {...field} - /> - - )} - /> - ) - } - - - - + ) + } + + + + + )} + /> )} - /> + ); } diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx index 550f60f7c4..9c1a5f5933 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx @@ -26,6 +26,7 @@ describe('', () => { id: 100, credential_type_id: 4, kind: 'scm', + name: 'alpha', }, }, }; @@ -216,6 +217,59 @@ describe('', () => { expect(formik.state.values.credential).toEqual(123); }); + test('manual subform should display expected fields', async () => { + const config = { + project_local_paths: ['foobar', 'qux'], + project_base_dir: 'dir/foo/bar', + }; + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { config }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + const playbookDirectorySelect = wrapper.find( + 'FormGroup[label="Playbook Directory"] FormSelect' + ); + await act(async () => { + playbookDirectorySelect + .props() + .onChange('foobar', { target: { name: 'foobar' } }); + }); + expect(wrapper.find('FormGroup[label="Project Base Path"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Playbook Directory"]').length).toBe( + 1 + ); + }); + + test('manual subform should display warning message when playbook directory is empty', async () => { + const config = { + project_local_paths: [], + project_base_dir: 'dir/foo/bar', + }; + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { config }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ManualSubForm Alert').length).toBe(1); + }); + test('should reset scm subform values when scm type changes', async () => { await act(async () => { wrapper = mountWithContexts( diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ManualSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ManualSubForm.jsx new file mode 100644 index 0000000000..260f3906ff --- /dev/null +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ManualSubForm.jsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Field } from 'formik'; +import AnsibleSelect from '@components/AnsibleSelect'; +import FormField, { FieldTooltip } from '@components/FormField'; +import { FormGroup, Alert } from '@patternfly/react-core'; +import { BrandName } from '../../../../variables'; + +// Setting BrandName to a variable here is necessary to get the jest tests +// passing. Attempting to use BrandName in the template literal results +// in failing tests. +const brandName = BrandName; + +const ManualSubForm = ({ + i18n, + localPath, + project_base_dir, + project_local_paths, +}) => { + const localPaths = [...new Set([...project_local_paths, localPath])]; + const options = [ + { + value: '', + key: '', + label: i18n._(t`Choose a Playbook Directory`), + }, + ...localPaths + .filter(path => path) + .map(path => ({ + value: path, + key: path, + label: path, + })), + ]; + + return ( + <> + {options.length === 1 && ( + + {i18n._(t` + There are no available playbook directories in ${project_base_dir}. + Either that directory is empty, or all of the contents are already + assigned to other projects. Create a new directory there and make + sure the playbook files can be read by the "awx" system user, + or have ${brandName} directly retrieve your playbooks from + source control using the SCM Type option above.`)} + + )} + + {i18n._(t`Base path used for locating playbooks. Directories + found inside this path will be listed in the playbook directory drop-down. + Together the base path and selected playbook directory provide the full + path used to locate playbooks.`)} +
+
+ {i18n._(t`Change PROJECTS_ROOT when deploying + ${brandName} to change this location.`)} + + } + /> + {options.length !== 1 && ( + ( + + + { + form.setFieldValue('local_path', value); + }} + /> + + )} + /> + )} + + ); +}; + +export default withI18n()(ManualSubForm); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js index 9443f17054..51386fe017 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js @@ -1,5 +1,6 @@ +export { SubFormTitle } from './SharedFields'; export { default as GitSubForm } from './GitSubForm'; export { default as HgSubForm } from './HgSubForm'; -export { default as SvnSubForm } from './SvnSubForm'; export { default as InsightsSubForm } from './InsightsSubForm'; -export { SubFormTitle } from './SharedFields'; +export { default as ManualSubForm } from './ManualSubForm'; +export { default as SvnSubForm } from './SvnSubForm';