diff --git a/awx/sso/conf.py b/awx/sso/conf.py
index 9bd033146b..eb944c3052 100644
--- a/awx/sso/conf.py
+++ b/awx/sso/conf.py
@@ -898,7 +898,7 @@ register(
field_class=fields.CharField,
allow_blank=True,
default='',
- label=_('GitHub OAuth2 Secret'),
+ label=_('GitHub Enterprise OAuth2 Secret'),
help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise developer application.'),
category=_('GitHub OAuth2'),
category_slug='github-enterprise',
@@ -952,7 +952,7 @@ register(
field_class=fields.CharField,
allow_blank=False,
default='',
- label=_('GitHub Enterprise URL'),
+ label=_('GitHub Enterprise Organization URL'),
help_text=_('The URL for your Github Enteprise.'),
category=_('GitHub Enterprise OAuth2'),
category_slug='github-enterprise-org',
@@ -963,7 +963,7 @@ register(
field_class=fields.CharField,
allow_blank=False,
default='',
- label=_('GitHub Enterprise API URL'),
+ label=_('GitHub Enterprise Organization API URL'),
help_text=_('The API URL for your GitHub Enterprise.'),
category=_('GitHub Enterprise OAuth2'),
category_slug='github-enterprise-org',
@@ -1052,8 +1052,8 @@ register(
field_class=fields.CharField,
allow_blank=False,
default='',
- label=_('GitHub Enterprise URL'),
- help_text=_('The URL for your Github Enteprise.'),
+ label=_('GitHub Enterprise Team URL'),
+ help_text=_('The URL for your Github Enterprise.'),
category=_('GitHub Enterprise OAuth2'),
category_slug='github-enterprise-team',
)
@@ -1063,7 +1063,7 @@ register(
field_class=fields.CharField,
allow_blank=False,
default='',
- label=_('GitHub Enterprise API URL'),
+ label=_('GitHub Enterprise Team API URL'),
help_text=_('The API URL for your GitHub Enterprise.'),
category=_('GitHub Enterprise OAuth2'),
category_slug='github-enterprise-team',
diff --git a/awx/ui_next/src/screens/Login/Login.jsx b/awx/ui_next/src/screens/Login/Login.jsx
index cad87f1dfa..8887d7351f 100644
--- a/awx/ui_next/src/screens/Login/Login.jsx
+++ b/awx/ui_next/src/screens/Login/Login.jsx
@@ -165,6 +165,41 @@ function AWXLogin({ alt, i18n, isAuthenticated }) {
);
}
+ if (authKey === 'github-enterprise') {
+ return (
+
+
+
+
+
+ );
+ }
+ if (authKey === 'github-enterprise-org') {
+ return (
+
+
+
+
+
+ );
+ }
+ if (authKey === 'github-enterprise-team') {
+ return (
+
+
+
+
+
+ );
+ }
if (authKey === 'google-oauth2') {
return (
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx
index 03d92e5899..aa23ae58ce 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx
@@ -8,6 +8,9 @@ import GitHubDetail from './GitHubDetail';
import GitHubEdit from './GitHubEdit';
import GitHubOrgEdit from './GitHubOrgEdit';
import GitHubTeamEdit from './GitHubTeamEdit';
+import GitHubEnterpriseEdit from './GitHubEnterpriseEdit';
+import GitHubEnterpriseOrgEdit from './GitHubEnterpriseOrgEdit';
+import GitHubEnterpriseTeamEdit from './GitHubEnterpriseTeamEdit';
function GitHub({ i18n }) {
const baseURL = '/settings/github';
@@ -40,6 +43,15 @@ function GitHub({ i18n }) {
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx
index 68572d6c35..12a2a95261 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx
@@ -48,6 +48,44 @@ describe('', () => {
SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {},
},
});
+ SettingsAPI.readCategory.mockResolvedValueOnce({
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: 'https://localhost/url',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: 'https://localhost/apiurl',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: 'ent_key',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '$encrypted',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: {},
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: {},
+ },
+ });
+ SettingsAPI.readCategory.mockResolvedValueOnce({
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise-org/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: 'https://localhost/url',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: 'https://localhost/apiurl',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: 'ent_org_key',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: 'ent_org_name',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: {},
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: {},
+ },
+ });
+ SettingsAPI.readCategory.mockResolvedValueOnce({
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise-team/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: 'https://localhost/url',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: 'https://localhost/apiurl',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: 'ent_team_key',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: 'ent_team_id',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: {},
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: {},
+ },
+ });
});
afterEach(() => {
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx
index 5dc76348fb..816ae229b8 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx
@@ -31,21 +31,33 @@ function GitHubDetail({ i18n }) {
{ data: gitHubDefault },
{ data: gitHubOrganization },
{ data: gitHubTeam },
+ { data: gitHubEnterprise },
+ { data: gitHubEnterpriseOrganization },
+ { data: gitHubEnterpriseTeam },
] = await Promise.all([
SettingsAPI.readCategory('github'),
SettingsAPI.readCategory('github-org'),
SettingsAPI.readCategory('github-team'),
+ SettingsAPI.readCategory('github-enterprise'),
+ SettingsAPI.readCategory('github-enterprise-org'),
+ SettingsAPI.readCategory('github-enterprise-team'),
]);
return {
default: gitHubDefault,
organization: gitHubOrganization,
team: gitHubTeam,
+ enterprise: gitHubEnterprise,
+ enterprise_organization: gitHubEnterpriseOrganization,
+ enterprise_team: gitHubEnterpriseTeam,
};
}, []),
{
default: null,
organization: null,
team: null,
+ enterprise: null,
+ enterprise_organization: null,
+ enterprise_team: null,
}
);
@@ -79,6 +91,21 @@ function GitHubDetail({ i18n }) {
link: `${baseURL}/team/details`,
id: 2,
},
+ {
+ name: i18n._(t`GitHub Enterprise`),
+ link: `${baseURL}/enterprise/details`,
+ id: 3,
+ },
+ {
+ name: i18n._(t`GitHub Enterprise Organization`),
+ link: `${baseURL}/enterprise_organization/details`,
+ id: 4,
+ },
+ {
+ name: i18n._(t`GitHub Enterprise Team`),
+ link: `${baseURL}/enterprise_team/details`,
+ id: 5,
+ },
];
if (!Object.keys(gitHubDetails).includes(category)) {
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx
index 11db4b57f0..3d2551e3a3 100644
--- a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx
@@ -51,6 +51,44 @@ const mockTeam = {
SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {},
},
};
+const mockEnterprise = {
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: 'https://localhost/enterpriseurl',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: 'https://localhost/enterpriseapi',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: 'foobar',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null,
+ },
+};
+const mockEnterpriseOrg = {
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise-org/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: 'https://localhost/orgurl',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: 'https://localhost/orgapi',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: 'foobar',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: 'foo',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null,
+ },
+};
+const mockEnterpriseTeam = {
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise-team/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: 'https://localhost/teamurl',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: 'https://localhost/teamapi',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: 'foobar',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: 'foo',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null,
+ },
+};
describe('', () => {
describe('Default', () => {
@@ -60,6 +98,9 @@ describe('', () => {
SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam);
useRouteMatch.mockImplementation(() => ({
url: '/settings/github/default/details',
path: '/settings/github/:category/details',
@@ -90,6 +131,9 @@ describe('', () => {
'GitHub Default',
'GitHub Organization',
'GitHub Team',
+ 'GitHub Enterprise',
+ 'GitHub Enterprise Organization',
+ 'GitHub Enterprise Team',
];
wrapper.find('RoutedTabs li').forEach((tab, index) => {
expect(tab.text()).toEqual(expectedTabs[index]);
@@ -149,6 +193,9 @@ describe('', () => {
SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam);
useRouteMatch.mockImplementation(() => ({
url: '/settings/github/organization/details',
path: '/settings/github/:category/details',
@@ -198,6 +245,9 @@ describe('', () => {
SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam);
useRouteMatch.mockImplementation(() => ({
url: '/settings/github/team/details',
path: '/settings/github/:category/details',
@@ -236,6 +286,199 @@ describe('', () => {
});
});
+ describe('Enterprise', () => {
+ let wrapper;
+
+ beforeAll(async () => {
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam);
+ useRouteMatch.mockImplementation(() => ({
+ url: '/settings/github/enterprise/details',
+ path: '/settings/github/:category/details',
+ params: { category: 'enterprise' },
+ }));
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ afterAll(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+
+ test('should render expected details', () => {
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise OAuth2 Callback URL',
+ 'https://towerhost/sso/complete/github-enterprise/'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise URL',
+ 'https://localhost/enterpriseurl'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise API URL',
+ 'https://localhost/enterpriseapi'
+ );
+ assertDetail(wrapper, 'GitHub Enterprise OAuth2 Key', 'foobar');
+ assertDetail(wrapper, 'GitHub Enterprise OAuth2 Secret', 'Encrypted');
+ assertVariableDetail(
+ wrapper,
+ 'GitHub Enterprise OAuth2 Organization Map',
+ '{}'
+ );
+ assertVariableDetail(wrapper, 'GitHub Enterprise OAuth2 Team Map', '{}');
+ });
+ });
+
+ describe('Enterprise Org', () => {
+ let wrapper;
+
+ beforeAll(async () => {
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam);
+ useRouteMatch.mockImplementation(() => ({
+ url: '/settings/github/enterprise_organization/details',
+ path: '/settings/github/:category/details',
+ params: { category: 'enterprise_organization' },
+ }));
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ afterAll(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+
+ test('should render expected details', () => {
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Organization OAuth2 Callback URL',
+ 'https://towerhost/sso/complete/github-enterprise-org/'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Organization URL',
+ 'https://localhost/orgurl'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Organization API URL',
+ 'https://localhost/orgapi'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Organization OAuth2 Key',
+ 'foobar'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Organization OAuth2 Secret',
+ 'Encrypted'
+ );
+ assertDetail(wrapper, 'GitHub Enterprise Organization Name', 'foo');
+ assertVariableDetail(
+ wrapper,
+ 'GitHub Enterprise Organization OAuth2 Organization Map',
+ '{}'
+ );
+ assertVariableDetail(
+ wrapper,
+ 'GitHub Enterprise Organization OAuth2 Team Map',
+ '{}'
+ );
+ });
+ });
+
+ describe('Enterprise Team', () => {
+ let wrapper;
+
+ beforeAll(async () => {
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg);
+ SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam);
+ useRouteMatch.mockImplementation(() => ({
+ url: '/settings/github/enterprise_team/details',
+ path: '/settings/github/:category/details',
+ params: { category: 'enterprise_team' },
+ }));
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ afterAll(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+
+ test('should render expected details', () => {
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Team OAuth2 Callback URL',
+ 'https://towerhost/sso/complete/github-enterprise-team/'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Team URL',
+ 'https://localhost/teamurl'
+ );
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Team API URL',
+ 'https://localhost/teamapi'
+ );
+ assertDetail(wrapper, 'GitHub Enterprise Team OAuth2 Key', 'foobar');
+ assertDetail(
+ wrapper,
+ 'GitHub Enterprise Team OAuth2 Secret',
+ 'Encrypted'
+ );
+ assertDetail(wrapper, 'GitHub Enterprise Team ID', 'foo');
+ assertVariableDetail(
+ wrapper,
+ 'GitHub Enterprise Team OAuth2 Organization Map',
+ '{}'
+ );
+ assertVariableDetail(
+ wrapper,
+ 'GitHub Enterprise Team OAuth2 Team Map',
+ '{}'
+ );
+ });
+ });
+
describe('Redirect', () => {
test('should render redirect when user navigates to erroneous category', async () => {
let wrapper;
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx
new file mode 100644
index 0000000000..3d6ee60c3e
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx
@@ -0,0 +1,151 @@
+import React, { useCallback, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { Formik } from 'formik';
+import { Form } from '@patternfly/react-core';
+import { CardBody } from '../../../../components/Card';
+import ContentError from '../../../../components/ContentError';
+import ContentLoading from '../../../../components/ContentLoading';
+import { FormSubmitError } from '../../../../components/FormField';
+import { FormColumnLayout } from '../../../../components/FormLayout';
+import { useSettings } from '../../../../contexts/Settings';
+import { RevertAllAlert, RevertFormActionGroup } from '../../shared';
+import {
+ EncryptedField,
+ InputField,
+ ObjectField,
+} from '../../shared/SharedFields';
+import { formatJson } from '../../shared/settingUtils';
+import useModal from '../../../../util/useModal';
+import useRequest from '../../../../util/useRequest';
+import { SettingsAPI } from '../../../../api';
+
+function GitHubEnterpriseEdit() {
+ const history = useHistory();
+ const { isModalOpen, toggleModal, closeModal } = useModal();
+ const { PUT: options } = useSettings();
+
+ const { isLoading, error, request: fetchGithub, result: github } = useRequest(
+ useCallback(async () => {
+ const { data } = await SettingsAPI.readCategory('github-enterprise');
+ const mergedData = {};
+ Object.keys(data).forEach(key => {
+ if (!options[key]) {
+ return;
+ }
+ mergedData[key] = options[key];
+ mergedData[key].value = data[key];
+ });
+ return mergedData;
+ }, [options]),
+ null
+ );
+
+ useEffect(() => {
+ fetchGithub();
+ }, [fetchGithub]);
+
+ const { error: submitError, request: submitForm } = useRequest(
+ useCallback(
+ async values => {
+ await SettingsAPI.updateAll(values);
+ history.push('/settings/github/enterprise/details');
+ },
+ [history]
+ ),
+ null
+ );
+
+ const handleSubmit = async form => {
+ await submitForm({
+ ...form,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: formatJson(
+ form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP
+ ),
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: formatJson(
+ form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP
+ ),
+ });
+ };
+
+ const handleRevertAll = async () => {
+ const defaultValues = Object.assign(
+ ...Object.entries(github).map(([key, value]) => ({
+ [key]: value.default,
+ }))
+ );
+ await submitForm(defaultValues);
+ closeModal();
+ };
+
+ const handleCancel = () => {
+ history.push('/settings/github/enterprise/details');
+ };
+
+ const initialValues = fields =>
+ Object.keys(fields).reduce((acc, key) => {
+ if (fields[key].type === 'list' || fields[key].type === 'nested object') {
+ const emptyDefault = fields[key].type === 'list' ? '[]' : '{}';
+ acc[key] = fields[key].value
+ ? JSON.stringify(fields[key].value, null, 2)
+ : emptyDefault;
+ } else {
+ acc[key] = fields[key].value ?? '';
+ }
+ return acc;
+ }, {});
+
+ return (
+
+ {isLoading && }
+ {!isLoading && error && }
+ {!isLoading && github && (
+
+ {formik => (
+
+ )}
+
+ )}
+
+ );
+}
+
+export default GitHubEnterpriseEdit;
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx
new file mode 100644
index 0000000000..f0bb46ab23
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx
@@ -0,0 +1,196 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../../testUtils/enzymeHelpers';
+import mockAllOptions from '../../shared/data.allSettingOptions.json';
+import { SettingsProvider } from '../../../../contexts/Settings';
+import { SettingsAPI } from '../../../../api';
+import GitHubEnterpriseEdit from './GitHubEnterpriseEdit';
+
+jest.mock('../../../../api/models/Settings');
+SettingsAPI.updateAll.mockResolvedValue({});
+SettingsAPI.readCategory.mockResolvedValue({
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null,
+ },
+});
+
+describe('', () => {
+ let wrapper;
+ let history;
+
+ afterEach(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+
+ beforeEach(async () => {
+ history = createMemoryHistory({
+ initialEntries: ['/settings/github/enterprise/edit'],
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ test('initially renders without crashing', () => {
+ expect(wrapper.find('GitHubEnterpriseEdit').length).toBe(1);
+ });
+
+ test('should display expected form fields', async () => {
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise URL"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise API URL"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Key"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Secret"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find(
+ 'FormGroup[label="GitHub Enterprise OAuth2 Organization Map"]'
+ ).length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Team Map"]')
+ .length
+ ).toBe(1);
+ });
+
+ test('should successfully send default values to api on form revert all', async () => {
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
+ await act(async () => {
+ wrapper
+ .find('button[aria-label="Revert all to default"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
+ await act(async () => {
+ wrapper
+ .find('RevertAllAlert button[aria-label="Confirm revert all"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null,
+ });
+ });
+
+ test('should successfully send request to api on form submission', async () => {
+ act(() => {
+ wrapper
+ .find(
+ 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET"] button[aria-label="Revert"]'
+ )
+ .invoke('onClick')();
+ wrapper
+ .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_URL')
+ .simulate('change', {
+ target: {
+ value: 'https://localhost',
+ name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL',
+ },
+ });
+ wrapper
+ .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP')
+ .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: 'https://localhost',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: {},
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: {
+ Default: {
+ users: false,
+ },
+ },
+ });
+ });
+
+ test('should navigate to github enterprise detail on successful submission', async () => {
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ expect(history.location.pathname).toEqual(
+ '/settings/github/enterprise/details'
+ );
+ });
+
+ test('should navigate to github enterprise detail when cancel is clicked', async () => {
+ await act(async () => {
+ wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
+ });
+ expect(history.location.pathname).toEqual(
+ '/settings/github/enterprise/details'
+ );
+ });
+
+ test('should display error message on unsuccessful submission', async () => {
+ const error = {
+ response: {
+ data: { detail: 'An error occurred' },
+ },
+ };
+ SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
+ expect(wrapper.find('FormSubmitError').length).toBe(0);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ wrapper.update();
+ expect(wrapper.find('FormSubmitError').length).toBe(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ });
+
+ test('should display ContentError on throw', async () => {
+ SettingsAPI.readCategory.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.find('ContentError').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js
new file mode 100644
index 0000000000..5b5377e423
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js
@@ -0,0 +1 @@
+export { default } from './GitHubEnterpriseEdit';
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx
new file mode 100644
index 0000000000..272b1866ff
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx
@@ -0,0 +1,157 @@
+import React, { useCallback, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { Formik } from 'formik';
+import { Form } from '@patternfly/react-core';
+import { CardBody } from '../../../../components/Card';
+import ContentError from '../../../../components/ContentError';
+import ContentLoading from '../../../../components/ContentLoading';
+import { FormSubmitError } from '../../../../components/FormField';
+import { FormColumnLayout } from '../../../../components/FormLayout';
+import { useSettings } from '../../../../contexts/Settings';
+import { RevertAllAlert, RevertFormActionGroup } from '../../shared';
+import {
+ EncryptedField,
+ InputField,
+ ObjectField,
+} from '../../shared/SharedFields';
+import { formatJson } from '../../shared/settingUtils';
+import useModal from '../../../../util/useModal';
+import useRequest from '../../../../util/useRequest';
+import { SettingsAPI } from '../../../../api';
+
+function GitHubEnterpriseOrgEdit() {
+ const history = useHistory();
+ const { isModalOpen, toggleModal, closeModal } = useModal();
+ const { PUT: options } = useSettings();
+
+ const { isLoading, error, request: fetchGithub, result: github } = useRequest(
+ useCallback(async () => {
+ const { data } = await SettingsAPI.readCategory('github-enterprise-org');
+ const mergedData = {};
+ Object.keys(data).forEach(key => {
+ if (!options[key]) {
+ return;
+ }
+ mergedData[key] = options[key];
+ mergedData[key].value = data[key];
+ });
+ return mergedData;
+ }, [options]),
+ null
+ );
+
+ useEffect(() => {
+ fetchGithub();
+ }, [fetchGithub]);
+
+ const { error: submitError, request: submitForm } = useRequest(
+ useCallback(
+ async values => {
+ await SettingsAPI.updateAll(values);
+ history.push('/settings/github/enterprise_organization/details');
+ },
+ [history]
+ ),
+ null
+ );
+
+ const handleSubmit = async form => {
+ await submitForm({
+ ...form,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: formatJson(
+ form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP
+ ),
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: formatJson(
+ form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP
+ ),
+ });
+ };
+
+ const handleRevertAll = async () => {
+ const defaultValues = Object.assign(
+ ...Object.entries(github).map(([key, value]) => ({
+ [key]: value.default,
+ }))
+ );
+ await submitForm(defaultValues);
+ closeModal();
+ };
+
+ const handleCancel = () => {
+ history.push('/settings/github/enterprise_organization/details');
+ };
+
+ const initialValues = fields =>
+ Object.keys(fields).reduce((acc, key) => {
+ if (fields[key].type === 'list' || fields[key].type === 'nested object') {
+ const emptyDefault = fields[key].type === 'list' ? '[]' : '{}';
+ acc[key] = fields[key].value
+ ? JSON.stringify(fields[key].value, null, 2)
+ : emptyDefault;
+ } else {
+ acc[key] = fields[key].value ?? '';
+ }
+ return acc;
+ }, {});
+
+ return (
+
+ {isLoading && }
+ {!isLoading && error && }
+ {!isLoading && github && (
+
+ {formik => (
+
+ )}
+
+ )}
+
+ );
+}
+
+export default GitHubEnterpriseOrgEdit;
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx
new file mode 100644
index 0000000000..278dd5d37e
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx
@@ -0,0 +1,212 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../../testUtils/enzymeHelpers';
+import mockAllOptions from '../../shared/data.allSettingOptions.json';
+import { SettingsProvider } from '../../../../contexts/Settings';
+import { SettingsAPI } from '../../../../api';
+import GitHubEnterpriseOrgEdit from './GitHubEnterpriseOrgEdit';
+
+jest.mock('../../../../api/models/Settings');
+SettingsAPI.updateAll.mockResolvedValue({});
+SettingsAPI.readCategory.mockResolvedValue({
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise-org/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null,
+ },
+});
+
+describe('', () => {
+ let wrapper;
+ let history;
+
+ afterEach(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+
+ beforeEach(async () => {
+ history = createMemoryHistory({
+ initialEntries: ['/settings/github/enterprise_organization/edit'],
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ test('initially renders without crashing', () => {
+ expect(wrapper.find('GitHubEnterpriseOrgEdit').length).toBe(1);
+ });
+
+ test('should display expected form fields', async () => {
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Organization URL"]')
+ .length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Organization API URL"]')
+ .length
+ ).toBe(1);
+ expect(
+ wrapper.find(
+ 'FormGroup[label="GitHub Enterprise Organization OAuth2 Key"]'
+ ).length
+ ).toBe(1);
+ expect(
+ wrapper.find(
+ 'FormGroup[label="GitHub Enterprise Organization OAuth2 Secret"]'
+ ).length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Organization Name"]')
+ .length
+ ).toBe(1);
+ expect(
+ wrapper.find(
+ 'FormGroup[label="GitHub Enterprise Organization OAuth2 Organization Map"]'
+ ).length
+ ).toBe(1);
+ expect(
+ wrapper.find(
+ 'FormGroup[label="GitHub Enterprise Organization OAuth2 Team Map"]'
+ ).length
+ ).toBe(1);
+ });
+
+ test('should successfully send default values to api on form revert all', async () => {
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
+ await act(async () => {
+ wrapper
+ .find('button[aria-label="Revert all to default"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
+ await act(async () => {
+ wrapper
+ .find('RevertAllAlert button[aria-label="Confirm revert all"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null,
+ });
+ });
+
+ test('should successfully send request to api on form submission', async () => {
+ act(() => {
+ wrapper
+ .find(
+ 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET"] button[aria-label="Revert"]'
+ )
+ .invoke('onClick')();
+ wrapper
+ .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL')
+ .simulate('change', {
+ target: {
+ value: 'https://localhost',
+ name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL',
+ },
+ });
+ wrapper
+ .find(
+ 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP'
+ )
+ .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: 'https://localhost',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: {},
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: {
+ Default: {
+ users: false,
+ },
+ },
+ });
+ });
+
+ test('should navigate to github enterprise org detail on successful submission', async () => {
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ expect(history.location.pathname).toEqual(
+ '/settings/github/enterprise_organization/details'
+ );
+ });
+
+ test('should navigate to github enterprise org detail when cancel is clicked', async () => {
+ await act(async () => {
+ wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
+ });
+ expect(history.location.pathname).toEqual(
+ '/settings/github/enterprise_organization/details'
+ );
+ });
+
+ test('should display error message on unsuccessful submission', async () => {
+ const error = {
+ response: {
+ data: { detail: 'An error occurred' },
+ },
+ };
+ SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
+ expect(wrapper.find('FormSubmitError').length).toBe(0);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ wrapper.update();
+ expect(wrapper.find('FormSubmitError').length).toBe(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ });
+
+ test('should display ContentError on throw', async () => {
+ SettingsAPI.readCategory.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.find('ContentError').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js
new file mode 100644
index 0000000000..e86b1f78f0
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js
@@ -0,0 +1 @@
+export { default } from './GitHubEnterpriseOrgEdit';
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx
new file mode 100644
index 0000000000..d9b725dc13
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx
@@ -0,0 +1,157 @@
+import React, { useCallback, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { Formik } from 'formik';
+import { Form } from '@patternfly/react-core';
+import { CardBody } from '../../../../components/Card';
+import ContentError from '../../../../components/ContentError';
+import ContentLoading from '../../../../components/ContentLoading';
+import { FormSubmitError } from '../../../../components/FormField';
+import { FormColumnLayout } from '../../../../components/FormLayout';
+import { useSettings } from '../../../../contexts/Settings';
+import { RevertAllAlert, RevertFormActionGroup } from '../../shared';
+import {
+ EncryptedField,
+ InputField,
+ ObjectField,
+} from '../../shared/SharedFields';
+import { formatJson } from '../../shared/settingUtils';
+import useModal from '../../../../util/useModal';
+import useRequest from '../../../../util/useRequest';
+import { SettingsAPI } from '../../../../api';
+
+function GitHubEnterpriseTeamEdit() {
+ const history = useHistory();
+ const { isModalOpen, toggleModal, closeModal } = useModal();
+ const { PUT: options } = useSettings();
+
+ const { isLoading, error, request: fetchGithub, result: github } = useRequest(
+ useCallback(async () => {
+ const { data } = await SettingsAPI.readCategory('github-enterprise-team');
+ const mergedData = {};
+ Object.keys(data).forEach(key => {
+ if (!options[key]) {
+ return;
+ }
+ mergedData[key] = options[key];
+ mergedData[key].value = data[key];
+ });
+ return mergedData;
+ }, [options]),
+ null
+ );
+
+ useEffect(() => {
+ fetchGithub();
+ }, [fetchGithub]);
+
+ const { error: submitError, request: submitForm } = useRequest(
+ useCallback(
+ async values => {
+ await SettingsAPI.updateAll(values);
+ history.push('/settings/github/enterprise_team/details');
+ },
+ [history]
+ ),
+ null
+ );
+
+ const handleSubmit = async form => {
+ await submitForm({
+ ...form,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: formatJson(
+ form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP
+ ),
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: formatJson(
+ form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP
+ ),
+ });
+ };
+
+ const handleRevertAll = async () => {
+ const defaultValues = Object.assign(
+ ...Object.entries(github).map(([key, value]) => ({
+ [key]: value.default,
+ }))
+ );
+ await submitForm(defaultValues);
+ closeModal();
+ };
+
+ const handleCancel = () => {
+ history.push('/settings/github/enterprise_team/details');
+ };
+
+ const initialValues = fields =>
+ Object.keys(fields).reduce((acc, key) => {
+ if (fields[key].type === 'list' || fields[key].type === 'nested object') {
+ const emptyDefault = fields[key].type === 'list' ? '[]' : '{}';
+ acc[key] = fields[key].value
+ ? JSON.stringify(fields[key].value, null, 2)
+ : emptyDefault;
+ } else {
+ acc[key] = fields[key].value ?? '';
+ }
+ return acc;
+ }, {});
+
+ return (
+
+ {isLoading && }
+ {!isLoading && error && }
+ {!isLoading && github && (
+
+ {formik => (
+
+ )}
+
+ )}
+
+ );
+}
+
+export default GitHubEnterpriseTeamEdit;
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx
new file mode 100644
index 0000000000..764c9fa377
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx
@@ -0,0 +1,206 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../../testUtils/enzymeHelpers';
+import mockAllOptions from '../../shared/data.allSettingOptions.json';
+import { SettingsProvider } from '../../../../contexts/Settings';
+import { SettingsAPI } from '../../../../api';
+import GitHubEnterpriseTeamEdit from './GitHubEnterpriseTeamEdit';
+
+jest.mock('../../../../api/models/Settings');
+SettingsAPI.updateAll.mockResolvedValue({});
+SettingsAPI.readCategory.mockResolvedValue({
+ data: {
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL:
+ 'https://towerhost/sso/complete/github-enterprise-team/',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '$encrypted$',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null,
+ },
+});
+
+describe('', () => {
+ let wrapper;
+ let history;
+
+ afterEach(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+
+ beforeEach(async () => {
+ history = createMemoryHistory({
+ initialEntries: ['/settings/github/enterprise_team/edit'],
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ test('initially renders without crashing', () => {
+ expect(wrapper.find('GitHubEnterpriseTeamEdit').length).toBe(1);
+ });
+
+ test('should display expected form fields', async () => {
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Team URL"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Team API URL"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Key"]')
+ .length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Secret"]')
+ .length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Team ID"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find(
+ 'FormGroup[label="GitHub Enterprise Team OAuth2 Organization Map"]'
+ ).length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Team Map"]')
+ .length
+ ).toBe(1);
+ });
+
+ test('should successfully send default values to api on form revert all', async () => {
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
+ await act(async () => {
+ wrapper
+ .find('button[aria-label="Revert all to default"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
+ await act(async () => {
+ wrapper
+ .find('RevertAllAlert button[aria-label="Confirm revert all"]')
+ .invoke('onClick')();
+ });
+ wrapper.update();
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null,
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null,
+ });
+ });
+
+ test('should successfully send request to api on form submission', async () => {
+ act(() => {
+ wrapper
+ .find(
+ 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET"] button[aria-label="Revert"]'
+ )
+ .invoke('onClick')();
+ wrapper
+ .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL')
+ .simulate('change', {
+ target: {
+ value: 'https://localhost',
+ name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL',
+ },
+ });
+ wrapper
+ .find(
+ 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP'
+ )
+ .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}');
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: 'https://localhost',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '',
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: {},
+ SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: {
+ Default: {
+ users: false,
+ },
+ },
+ });
+ });
+
+ test('should navigate to github enterprise team detail on successful submission', async () => {
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ expect(history.location.pathname).toEqual(
+ '/settings/github/enterprise_team/details'
+ );
+ });
+
+ test('should navigate to github enterprise team detail when cancel is clicked', async () => {
+ await act(async () => {
+ wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
+ });
+ expect(history.location.pathname).toEqual(
+ '/settings/github/enterprise_team/details'
+ );
+ });
+
+ test('should display error message on unsuccessful submission', async () => {
+ const error = {
+ response: {
+ data: { detail: 'An error occurred' },
+ },
+ };
+ SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
+ expect(wrapper.find('FormSubmitError').length).toBe(0);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
+ await act(async () => {
+ wrapper.find('Form').invoke('onSubmit')();
+ });
+ wrapper.update();
+ expect(wrapper.find('FormSubmitError').length).toBe(1);
+ expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
+ });
+
+ test('should display ContentError on throw', async () => {
+ SettingsAPI.readCategory.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.find('ContentError').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js
new file mode 100644
index 0000000000..002e319910
--- /dev/null
+++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js
@@ -0,0 +1 @@
+export { default } from './GitHubEnterpriseTeamEdit';
diff --git a/awx/ui_next/src/screens/Setting/Settings.jsx b/awx/ui_next/src/screens/Setting/Settings.jsx
index a535384afa..8b9c2db334 100644
--- a/awx/ui_next/src/screens/Setting/Settings.jsx
+++ b/awx/ui_next/src/screens/Setting/Settings.jsx
@@ -57,6 +57,17 @@ function Settings({ i18n }) {
'/settings/github/team': i18n._(t`GitHub Team`),
'/settings/github/team/details': i18n._(t`Details`),
'/settings/github/team/edit': i18n._(t`Edit Details`),
+ '/settings/github/enterprise': i18n._(t`GitHub Enterprise`),
+ '/settings/github/enterprise/details': i18n._(t`Details`),
+ '/settings/github/enterprise/edit': i18n._(t`Edit Details`),
+ '/settings/github/enterprise_organization': i18n._(
+ t`GitHub Enterprise Organization`
+ ),
+ '/settings/github/enterprise_organization/details': i18n._(t`Details`),
+ '/settings/github/enterprise_organization/edit': i18n._(t`Edit Details`),
+ '/settings/github/enterprise_team': i18n._(t`GitHub Enterprise Team`),
+ '/settings/github/enterprise_team/details': i18n._(t`Details`),
+ '/settings/github/enterprise_team/edit': i18n._(t`Edit Details`),
'/settings/google_oauth2': i18n._(t`Google OAuth2`),
'/settings/google_oauth2/details': i18n._(t`Details`),
'/settings/google_oauth2/edit': i18n._(t`Edit Details`),