diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
index 4d5cf86cb0..6872e09784 100644
--- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
+++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import { withI18n } from '@lingui/react';
-import { bool, func, number, string } from 'prop-types';
+import { bool, func, number, string, oneOfType } from 'prop-types';
import { CredentialsAPI } from '@api';
import { Credential } from '@types';
import { mergeParams } from '@util/qs';
@@ -46,7 +46,7 @@ function CredentialLookup({
}
CredentialLookup.propTypes = {
- credentialTypeId: number.isRequired,
+ credentialTypeId: oneOfType([number, string]).isRequired,
helperTextInvalid: string,
isValid: bool,
label: string.isRequired,
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 f315294b30..2b9857bc89 100644
--- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx
@@ -3,11 +3,12 @@ import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import ProjectAdd from './ProjectAdd';
-import { ProjectsAPI } from '@api';
+import { ProjectsAPI, CredentialTypesAPI } from '@api';
jest.mock('@api');
describe('', () => {
+ let wrapper;
const projectData = {
name: 'foo',
description: 'bar',
@@ -22,12 +23,68 @@ describe('', () => {
custom_virtualenv: '/venv/custom-env',
};
+ const projectOptionsResolve = {
+ data: {
+ actions: {
+ GET: {
+ scm_type: {
+ choices: [
+ ['', 'Manual'],
+ ['git', 'Git'],
+ ['hg', 'Mercurial'],
+ ['svn', 'Subversion'],
+ ['insights', 'Red Hat Insights'],
+ ],
+ },
+ },
+ },
+ },
+ };
+
+ const scmCredentialResolve = {
+ data: {
+ results: [
+ {
+ id: 4,
+ name: 'Source Control',
+ kind: 'scm',
+ },
+ ],
+ },
+ };
+
+ const insightsCredentialResolve = {
+ data: {
+ results: [
+ {
+ id: 5,
+ name: 'Insights',
+ kind: 'insights',
+ },
+ ],
+ },
+ };
+
+ beforeEach(async () => {
+ await ProjectsAPI.readOptions.mockImplementation(
+ () => projectOptionsResolve
+ );
+ await CredentialTypesAPI.read.mockImplementationOnce(
+ () => scmCredentialResolve
+ );
+ await CredentialTypesAPI.read.mockImplementationOnce(
+ () => insightsCredentialResolve
+ );
+ });
+
afterEach(() => {
jest.clearAllMocks();
});
- test('initially renders successfully', () => {
- const wrapper = mountWithContexts();
+ test('initially renders successfully', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
expect(wrapper.length).toBe(1);
});
@@ -35,11 +92,10 @@ describe('', () => {
ProjectsAPI.create.mockResolvedValueOnce({
data: { ...projectData },
});
- let wrapper;
await act(async () => {
wrapper = mountWithContexts();
});
- await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
const formik = wrapper.find('Formik').instance();
const changeState = new Promise(resolve => {
formik.setState(
@@ -57,11 +113,10 @@ describe('', () => {
test('handleSubmit should throw an error', async () => {
ProjectsAPI.create.mockImplementation(() => Promise.reject(new Error()));
- let wrapper;
await act(async () => {
wrapper = mountWithContexts();
});
- await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
const formik = wrapper.find('Formik').instance();
const changeState = new Promise(resolve => {
formik.setState(
@@ -81,21 +136,26 @@ describe('', () => {
expect(wrapper.find('ProjectAdd .formSubmitError').length).toBe(1);
});
- test('CardHeader close button should navigate to projects list', () => {
+ test('CardHeader close button should navigate to projects list', async () => {
const history = createMemoryHistory();
- const wrapper = mountWithContexts(, {
- context: { router: { history } },
- }).find('ProjectAdd CardHeader');
+ await act(async () => {
+ wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ }).find('ProjectAdd CardHeader');
+ });
wrapper.find('CardCloseButton').simulate('click');
expect(history.location.pathname).toEqual('/projects');
});
- test('CardBody cancel button should navigate to projects list', () => {
+ test('CardBody cancel button should navigate to projects list', async () => {
const history = createMemoryHistory();
- const wrapper = mountWithContexts(, {
- context: { router: { history } },
- }).find('ProjectAdd CardBody');
- wrapper.find('button[aria-label="Cancel"]').simulate('click');
+ await act(async () => {
+ wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ });
+ });
+ await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
+ wrapper.find('ProjectAdd button[aria-label="Cancel"]').simulate('click');
expect(history.location.pathname).toEqual('/projects');
});
});
diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx
index 16593e737e..7478954a5e 100644
--- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx
+++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { withRouter, Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
@@ -11,11 +11,14 @@ import {
Title as _Title,
} from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect';
+import ContentError from '@components/ContentError';
+import ContentLoading from '@components/ContentLoading';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormField, { CheckboxField, FieldTooltip } from '@components/FormField';
import FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import CredentialLookup from '@components/Lookup/CredentialLookup';
+import { CredentialTypesAPI, ProjectsAPI } from '@api';
import { required } from '@util/validators';
import styled from 'styled-components';
@@ -41,9 +44,54 @@ const Title = styled(_Title)`
function ProjectForm(props) {
const { values, handleCancel, handleSubmit, i18n } = props;
+ const [contentError, setContentError] = useState(null);
+ const [hasContentLoading, setHasContentLoading] = useState(true);
const [organization, setOrganization] = useState(null);
- const [scmCredential, setScmCredential] = useState(null);
- const [insightsCredential, setInsightsCredential] = useState(null);
+ const [scmTypeOptions, setScmTypeOptions] = useState(null);
+ const [scmCredential, setScmCredential] = useState({
+ typeId: null,
+ value: null,
+ });
+ const [insightsCredential, setInsightsCredential] = useState({
+ typeId: null,
+ value: null,
+ });
+
+ useEffect(() => {
+ async function fetchCredTypeId(params) {
+ try {
+ const {
+ data: {
+ results: [credential],
+ },
+ } = await CredentialTypesAPI.read(params);
+ return credential.id;
+ } catch (error) {
+ setContentError(error);
+ return null;
+ }
+ }
+
+ async function fetchData() {
+ const insightsTypeId = await fetchCredTypeId({ name: 'Insights' });
+ const scmTypeId = await fetchCredTypeId({ kind: 'scm' });
+ const {
+ data: {
+ actions: {
+ GET: {
+ scm_type: { choices },
+ },
+ },
+ },
+ } = await ProjectsAPI.readOptions();
+ setInsightsCredential({ typeId: insightsTypeId });
+ setScmCredential({ typeId: scmTypeId });
+ setScmTypeOptions(choices);
+ setHasContentLoading(false);
+ }
+
+ fetchData();
+ }, []);
const resetScmTypeFields = (value, form) => {
if (form.initialValues.scm_type === value) {
@@ -67,36 +115,6 @@ function ProjectForm(props) {
});
};
- const scmTypeOptions = [
- {
- value: '',
- key: '',
- label: i18n._(t`Choose a SCM Type`),
- isDisabled: true,
- },
- { value: 'manual', key: 'manual', label: i18n._(t`Manual`) },
- {
- value: 'git',
- key: 'git',
- label: i18n._(t`Git`),
- },
- {
- value: 'hg',
- key: 'hg',
- label: i18n._(t`Mercurial`),
- },
- {
- value: 'svn',
- key: 'svn',
- label: i18n._(t`Subversion`),
- },
- {
- value: 'insights',
- key: 'insights',
- label: i18n._(t`Red Hat Insights`),
- },
- ];
-
const gitScmTooltip = (
{i18n._(t`Example URLs for GIT SCM include:`)}
@@ -153,6 +171,14 @@ function ProjectForm(props) {
svn: i18n._(t`Revision #`),
};
+ if (hasContentLoading) {
+ return ;
+ }
+
+ if (contentError) {
+ return ;
+ }
+
return (