From 7662a67a658516aaa12c9f47f84aae80011f6a38 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Fri, 4 Dec 2020 16:01:25 -0500 Subject: [PATCH 1/2] Add all github category setting edit forms --- .../src/screens/Setting/GitHub/GitHub.jsx | 10 +- .../screens/Setting/GitHub/GitHub.test.jsx | 99 ++++++++-- .../Setting/GitHub/GitHubEdit/GitHubEdit.jsx | 152 ++++++++++++-- .../GitHub/GitHubEdit/GitHubEdit.test.jsx | 165 +++++++++++++++- .../GitHub/GitHubOrgEdit/GitHubOrgEdit.jsx | 147 ++++++++++++++ .../GitHubOrgEdit/GitHubOrgEdit.test.jsx | 186 ++++++++++++++++++ .../Setting/GitHub/GitHubOrgEdit/index.js | 1 + .../GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx | 147 ++++++++++++++ .../GitHubTeamEdit/GitHubTeamEdit.test.jsx | 177 +++++++++++++++++ .../Setting/GitHub/GitHubTeamEdit/index.js | 1 + 10 files changed, 1048 insertions(+), 37 deletions(-) create mode 100644 awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.jsx create mode 100644 awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx create mode 100644 awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/index.js create mode 100644 awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx create mode 100644 awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx create mode 100644 awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/index.js diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx index c134adca9c..03d92e5899 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx @@ -6,6 +6,8 @@ import { PageSection, Card } from '@patternfly/react-core'; import ContentError from '../../../components/ContentError'; import GitHubDetail from './GitHubDetail'; import GitHubEdit from './GitHubEdit'; +import GitHubOrgEdit from './GitHubOrgEdit'; +import GitHubTeamEdit from './GitHubTeamEdit'; function GitHub({ i18n }) { const baseURL = '/settings/github'; @@ -29,9 +31,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 37a1c62a67..68572d6c35 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx @@ -5,33 +5,94 @@ import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; -import GitHub from './GitHub'; import { SettingsAPI } from '../../../api'; +import { SettingsProvider } from '../../../contexts/Settings'; +import mockAllOptions from '../shared/data.allSettingOptions.json'; +import GitHub from './GitHub'; jest.mock('../../../api/models/Settings'); -SettingsAPI.readCategory.mockResolvedValue({ - data: {}, -}); describe('', () => { let wrapper; + beforeEach(() => { + SettingsAPI.readCategory.mockResolvedValueOnce({ + data: { + SOCIAL_AUTH_GITHUB_CALLBACK_URL: + 'https://towerhost/sso/complete/github/', + SOCIAL_AUTH_GITHUB_KEY: 'mock github key', + SOCIAL_AUTH_GITHUB_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_TEAM_MAP: null, + }, + }); + SettingsAPI.readCategory.mockResolvedValueOnce({ + data: { + SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL: + 'https://towerhost/sso/complete/github-org/', + SOCIAL_AUTH_GITHUB_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ORG_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: null, + }, + }); + SettingsAPI.readCategory.mockResolvedValueOnce({ + data: { + SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL: + 'https://towerhost/sso/complete/github-team/', + SOCIAL_AUTH_GITHUB_TEAM_KEY: 'OAuth2 key (Client ID)', + SOCIAL_AUTH_GITHUB_TEAM_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_TEAM_ID: 'team_id', + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {}, + }, + }); + }); + afterEach(() => { wrapper.unmount(); jest.clearAllMocks(); }); - test('should render github details', async () => { + test('should render github default details', async () => { const history = createMemoryHistory({ initialEntries: ['/settings/github/'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('GitHubDetail').length).toBe(1); + expect(wrapper.find('Detail[label="GitHub OAuth2 Key"]').length).toBe(1); + }); + + test('should redirect to github organization category details', async () => { + const history = createMemoryHistory({ + initialEntries: ['/settings/github/organization'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('GitHubDetail').length).toBe(1); + expect( + wrapper.find('Detail[label="GitHub Organization OAuth2 Key"]').length + ).toBe(1); }); test('should render github edit', async () => { @@ -39,9 +100,14 @@ describe('', () => { initialEntries: ['/settings/github/default/edit'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('GitHubEdit').length).toBe(1); @@ -52,9 +118,14 @@ describe('', () => { initialEntries: ['/settings/github/foo/bar'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('ContentError').length).toBe(1); }); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.jsx index 07a6f45015..f1b562043d 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.jsx @@ -1,25 +1,141 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { Button } from '@patternfly/react-core'; -import { CardBody, CardActionsRow } from '../../../../components/Card'; +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 GitHubEdit() { + 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'); + 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/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_TEAM_MAP: formatJson(form.SOCIAL_AUTH_GITHUB_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/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; + }, {}); -function GitHubEdit({ i18n }) { return ( - {i18n._(t`Edit form coming soon :)`)} - - - + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )}
); } -export default withI18n()(GitHubEdit); +export default GitHubEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx index 539932c99a..f864f1f6ca 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx @@ -1,16 +1,173 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +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 GitHubEdit from './GitHubEdit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_CALLBACK_URL: 'https://foo/complete/github/', + SOCIAL_AUTH_GITHUB_KEY: 'mock github key', + SOCIAL_AUTH_GITHUB_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: { + Default: { + users: true, + }, + }, + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/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('GitHubEdit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="GitHub OAuth2 Key"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="GitHub OAuth2 Secret"]').length).toBe( + 1 + ); + expect( + wrapper.find('FormGroup[label="GitHub OAuth2 Organization Map"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub 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_KEY: '', + SOCIAL_AUTH_GITHUB_SECRET: '', + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GITHUB_KEY').simulate('change', { + target: { value: 'new key', name: 'SOCIAL_AUTH_GITHUB_KEY' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_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_KEY: 'new key', + SOCIAL_AUTH_GITHUB_SECRET: '', + SOCIAL_AUTH_GITHUB_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github default detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/github/details'); + }); + + test('should navigate to github default detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/github/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/GitHubOrgEdit/GitHubOrgEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.jsx new file mode 100644 index 0000000000..6224acb5b7 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.jsx @@ -0,0 +1,147 @@ +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 GitHubOrgEdit() { + 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-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/organization/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_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/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 => ( +
+ + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubOrgEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx new file mode 100644 index 0000000000..57396c43d1 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx @@ -0,0 +1,186 @@ +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 GitHubOrgEdit from './GitHubOrgEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL: + 'https://towerhost/sso/complete/github-org/', + SOCIAL_AUTH_GITHUB_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ORG_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/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('GitHubOrgEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Organization OAuth2 Key"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Organization OAuth2 Secret"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Organization Name"]').length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Organization OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub 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_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ORG_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GITHUB_ORG_NAME').simulate('change', { + target: { value: 'new org', name: 'SOCIAL_AUTH_GITHUB_ORG_NAME' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_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_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ORG_NAME: 'new org', + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github organization detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/organization/details' + ); + }); + + test('should navigate to github organization detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/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/GitHubOrgEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/index.js new file mode 100644 index 0000000000..1652804b44 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubOrgEdit'; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx new file mode 100644 index 0000000000..f898539283 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx @@ -0,0 +1,147 @@ +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 GitHubTeamEdit() { + 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-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/team/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_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/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 => ( +
+ + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubTeamEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx new file mode 100644 index 0000000000..bbc36f8948 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx @@ -0,0 +1,177 @@ +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 GitHubTeamEdit from './GitHubTeamEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL: + 'https://towerhost/sso/complete/github-team/', + SOCIAL_AUTH_GITHUB_TEAM_KEY: 'OAuth2 key (Client ID)', + SOCIAL_AUTH_GITHUB_TEAM_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_TEAM_ID: 'team_id', + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {}, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/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('GitHubTeamEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Team OAuth2 Key"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Team OAuth2 Secret"]').length + ).toBe(1); + expect(wrapper.find('FormGroup[label="GitHub Team ID"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Team OAuth2 Organization Map"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub 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_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_TEAM_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GITHUB_TEAM_ID').simulate('change', { + target: { value: '12345', name: 'SOCIAL_AUTH_GITHUB_TEAM_ID' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\ntrue\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_TEAM_KEY: 'OAuth2 key (Client ID)', + SOCIAL_AUTH_GITHUB_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_TEAM_ID: '12345', + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: { + Default: { + users: true, + }, + }, + }); + }); + + test('should navigate to github team detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/github/team/details'); + }); + + test('should navigate to github 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/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/GitHubTeamEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/index.js new file mode 100644 index 0000000000..ba00e5355b --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubTeamEdit'; From 1c2605be8e8f18bf9a0a8ebf6feff2a55e769cd0 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Mon, 11 Jan 2021 16:38:32 -0500 Subject: [PATCH 2/2] Add ouiaId props to button and alert components --- .../ActivityStreamDetail/ActivityStreamDetail.jsx | 1 + .../screens/Setting/AzureAD/AzureADDetail/AzureADDetail.jsx | 1 + .../src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx | 1 + .../GoogleOAuth2/GoogleOAuth2Detail/GoogleOAuth2Detail.jsx | 1 + awx/ui_next/src/screens/Setting/Jobs/JobsDetail/JobsDetail.jsx | 1 + awx/ui_next/src/screens/Setting/LDAP/LDAPDetail/LDAPDetail.jsx | 1 + .../screens/Setting/License/LicenseDetail/LicenseDetail.jsx | 1 + .../screens/Setting/Logging/LoggingDetail/LoggingDetail.jsx | 1 + .../src/screens/Setting/Logging/LoggingEdit/LoggingEdit.jsx | 1 + .../Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx | 1 + .../src/screens/Setting/RADIUS/RADIUSDetail/RADIUSDetail.jsx | 1 + awx/ui_next/src/screens/Setting/SAML/SAMLDetail/SAMLDetail.jsx | 1 + .../src/screens/Setting/TACACS/TACACSDetail/TACACSDetail.jsx | 1 + awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx | 1 + awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx | 1 + awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx | 3 +++ .../src/screens/Setting/shared/RevertFormActionGroup.jsx | 3 +++ awx/ui_next/src/screens/Setting/shared/SharedFields.jsx | 1 + 18 files changed, 22 insertions(+) diff --git a/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamDetail/ActivityStreamDetail.jsx b/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamDetail/ActivityStreamDetail.jsx index d5e6afe101..55bcb74db0 100644 --- a/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamDetail/ActivityStreamDetail.jsx +++ b/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamDetail/ActivityStreamDetail.jsx @@ -86,6 +86,7 @@ function ActivityStreamDetail({ i18n }) { diff --git a/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx b/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx index ef458e5163..e65f176b83 100644 --- a/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx @@ -94,6 +94,7 @@ function UIDetail({ i18n }) { aria-label={i18n._(t`Edit`)} component={Link} to="/settings/ui/edit" + ouiaId="edit-button" > {i18n._(t`Edit`)} diff --git a/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx b/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx index a1c3903f53..4ec68182fc 100644 --- a/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx +++ b/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx @@ -33,6 +33,7 @@ function LoggingTestAlert({ i18n, successResponse, errorResponse, onClose }) { {testMessage && ( } + ouiaId="logging-test-alert" title={successResponse ? i18n._(t`Success`) : i18n._(t`Error`)} variant={successResponse ? 'success' : 'danger'} > diff --git a/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx b/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx index 55b23437b9..46ed00e8d6 100644 --- a/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx +++ b/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx @@ -11,12 +11,14 @@ function RevertAllAlert({ i18n, onClose, onRevertAll }) { title={i18n._(t`Revert settings`)} variant="info" onClose={onClose} + ouiaId="revert-all-modal" actions={[ , @@ -25,6 +27,7 @@ function RevertAllAlert({ i18n, onClose, onRevertAll }) { variant="secondary" aria-label={i18n._(t`Cancel revert`)} onClick={onClose} + ouiaId="cancel-revert-all-button" > {i18n._(t`Cancel`)} , diff --git a/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx b/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx index 03e80d4eb1..1e707090e2 100644 --- a/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx +++ b/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx @@ -20,6 +20,7 @@ const RevertFormActionGroup = ({ variant="primary" type="button" onClick={onSubmit} + ouiaId="save-button" > {i18n._(t`Save`)} @@ -28,6 +29,7 @@ const RevertFormActionGroup = ({ variant="secondary" type="button" onClick={onRevert} + ouiaId="revert-all-button" > {i18n._(t`Revert all to default`)} @@ -37,6 +39,7 @@ const RevertFormActionGroup = ({ variant="secondary" type="button" onClick={onCancel} + ouiaId="cancel-button" > {i18n._(t`Cancel`)} diff --git a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx index c23a9da17a..2e27fd0ea3 100644 --- a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx +++ b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx @@ -90,6 +90,7 @@ const BooleanField = withI18n()( labelOff={i18n._(t`Off`)} onChange={checked => helpers.setValue(checked)} aria-label={ariaLabel || config.label} + ouiaId={ariaLabel || config.label} /> ) : null;