From 19ae4eadfbb87c9f7a4ce19f04f85a2ca0145fba Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 31 Aug 2020 15:23:32 -0400 Subject: [PATCH] Add galaxy credentials field to organizations form --- awx/ui_next/src/api/models/Organizations.js | 19 + .../components/Lookup/CredentialLookup.jsx | 17 +- .../src/screens/Organization/Organization.jsx | 383 +++++++++--------- .../Organization/Organization.test.jsx | 72 ++-- .../OrganizationAdd/OrganizationAdd.jsx | 10 +- .../OrganizationAdd/OrganizationAdd.test.jsx | 41 +- .../OrganizationDetail/OrganizationDetail.jsx | 19 + .../OrganizationEdit/OrganizationEdit.jsx | 37 +- .../screens/Organization/Organizations.jsx | 4 +- .../Organization/shared/OrganizationForm.jsx | 30 +- .../shared/OrganizationForm.test.jsx | 3 + 11 files changed, 400 insertions(+), 235 deletions(-) diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 76c15504dc..ce236067b4 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -24,6 +24,12 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { return this.http.options(`${this.baseUrl}${id}/teams/`); } + readGalaxyCredentials(id, params) { + return this.http.get(`${this.baseUrl}${id}/galaxy_credentials/`, { + params, + }); + } + createUser(id, data) { return this.http.post(`${this.baseUrl}${id}/users/`, data); } @@ -48,6 +54,19 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { { id: notificationId, disassociate: true } ); } + + associateGalaxyCredential(resourceId, credentialId) { + return this.http.post(`${this.baseUrl}${resourceId}/galaxy_credentials/`, { + id: credentialId, + }); + } + + disassociateGalaxyCredential(resourceId, credentialId) { + return this.http.post(`${this.baseUrl}${resourceId}/galaxy_credentials/`, { + id: credentialId, + disassociate: true, + }); + } } export default Organizations; diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index 43440d0d22..019fa0e573 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -1,5 +1,13 @@ import React, { useCallback, useEffect } from 'react'; -import { bool, func, node, number, string, oneOfType } from 'prop-types'; +import { + arrayOf, + bool, + func, + node, + number, + string, + oneOfType, +} from 'prop-types'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -36,6 +44,7 @@ function CredentialLookup({ tooltip, isDisabled, autoPopulate, + multiple, }) { const autoPopulateLookup = useAutoPopulateLookup(onChange); const { @@ -120,6 +129,7 @@ function CredentialLookup({ required={required} qsConfig={QS_CONFIG} isDisabled={isDisabled} + multiple={multiple} renderOptionsList={({ state, dispatch, canDelete }) => ( dispatch({ type: 'SELECT_ITEM', item })} deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })} + multiple={multiple} /> )} /> @@ -188,10 +199,11 @@ CredentialLookup.propTypes = { helperTextInvalid: node, isValid: bool, label: string.isRequired, + multiple: bool, onBlur: func, onChange: func.isRequired, required: bool, - value: Credential, + value: oneOfType([Credential, arrayOf(Credential)]), isDisabled: bool, autoPopulate: bool, }; @@ -201,6 +213,7 @@ CredentialLookup.defaultProps = { credentialTypeKind: '', helperTextInvalid: '', isValid: true, + multiple: false, onBlur: () => {}, required: false, value: null, diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx index d16113d8dc..1a2ee641c9 100644 --- a/awx/ui_next/src/screens/Organization/Organization.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.jsx @@ -1,9 +1,19 @@ -import React, { Component } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom'; +import { + Switch, + Route, + withRouter, + Redirect, + Link, + useLocation, + useParams, + useRouteMatch, +} from 'react-router-dom'; import { CaretLeftIcon } from '@patternfly/react-icons'; import { Card, PageSection } from '@patternfly/react-core'; +import useRequest from '../../util/useRequest'; import RoutedTabs from '../../components/RoutedTabs'; import ContentError from '../../components/ContentError'; import NotificationList from '../../components/NotificationList/NotificationList'; @@ -13,214 +23,207 @@ import OrganizationEdit from './OrganizationEdit'; import OrganizationTeams from './OrganizationTeams'; import { OrganizationsAPI } from '../../api'; -class Organization extends Component { - constructor(props) { - super(props); +function Organization({ i18n, setBreadcrumb, me }) { + const location = useLocation(); + const { id: organizationId } = useParams(); + const match = useRouteMatch(); + const initialUpdate = useRef(true); - this.state = { - organization: null, - hasContentLoading: true, - contentError: null, - isInitialized: false, - isNotifAdmin: false, - isAuditorOfThisOrg: false, - isAdminOfThisOrg: false, - }; - this.loadOrganization = this.loadOrganization.bind(this); - this.loadOrganizationAndRoles = this.loadOrganizationAndRoles.bind(this); - } - - async componentDidMount() { - await this.loadOrganizationAndRoles(); - this.setState({ isInitialized: true }); - } - - async componentDidUpdate(prevProps) { - const { location, match } = this.props; - const url = `/organizations/${match.params.id}/`; - - if ( - prevProps.location.pathname.startsWith(url) && - prevProps.location !== location && - location.pathname === `${url}details` - ) { - await this.loadOrganization(); - } - } - - async loadOrganizationAndRoles() { - const { match, setBreadcrumb } = this.props; - const id = parseInt(match.params.id, 10); - - this.setState({ contentError: null, hasContentLoading: true }); - try { - const [{ data }, notifAdminRes, auditorRes, adminRes] = await Promise.all( - [ - OrganizationsAPI.readDetail(id), - OrganizationsAPI.read({ - page_size: 1, - role_level: 'notification_admin_role', - }), - OrganizationsAPI.read({ id, role_level: 'auditor_role' }), - OrganizationsAPI.read({ id, role_level: 'admin_role' }), - ] - ); + const { + result: { organization }, + isLoading: organizationLoading, + error: organizationError, + request: loadOrganization, + } = useRequest( + useCallback(async () => { + const [{ data }, credentialsRes] = await Promise.all([ + OrganizationsAPI.readDetail(organizationId), + OrganizationsAPI.readGalaxyCredentials(organizationId), + ]); + data.galaxy_credentials = credentialsRes.data.results; setBreadcrumb(data); - this.setState({ + + return { organization: data, + }; + }, [setBreadcrumb, organizationId]), + { + organization: null, + } + ); + + const { + result: { isNotifAdmin, isAuditorOfThisOrg, isAdminOfThisOrg }, + isLoading: rolesLoading, + error: rolesError, + request: loadRoles, + } = useRequest( + useCallback(async () => { + const [notifAdminRes, auditorRes, adminRes] = await Promise.all([ + OrganizationsAPI.read({ + page_size: 1, + role_level: 'notification_admin_role', + }), + OrganizationsAPI.read({ + id: organizationId, + role_level: 'auditor_role', + }), + OrganizationsAPI.read({ + id: organizationId, + role_level: 'admin_role', + }), + ]); + + return { isNotifAdmin: notifAdminRes.data.results.length > 0, isAuditorOfThisOrg: auditorRes.data.results.length > 0, isAdminOfThisOrg: adminRes.data.results.length > 0, - }); - } catch (err) { - this.setState({ contentError: err }); - } finally { - this.setState({ hasContentLoading: false }); + }; + }, [organizationId]), + { + isNotifAdmin: false, + isAuditorOfThisOrg: false, + isAdminOfThisOrg: false, } + ); + useEffect(() => { + loadOrganization(); + loadRoles(); + }, [loadOrganization, loadRoles]); + + useEffect(() => { + if (initialUpdate.current) { + initialUpdate.current = false; + return; + } + + if (location.pathname === `/organizations/${organizationId}/details`) { + loadOrganization(); + } + }, [loadOrganization, organizationId, location.pathname]); + + const canSeeNotificationsTab = + me.is_system_auditor || isNotifAdmin || isAuditorOfThisOrg; + const canToggleNotifications = + isNotifAdmin && + (me.is_system_auditor || isAuditorOfThisOrg || isAdminOfThisOrg); + + const tabsArray = [ + { + name: ( + <> + + {i18n._(t`Back to Organizations`)} + + ), + link: `/organizations`, + id: 99, + }, + { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, + { name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 }, + { name: i18n._(t`Teams`), link: `${match.url}/teams`, id: 2 }, + ]; + + if (canSeeNotificationsTab) { + tabsArray.push({ + name: i18n._(t`Notifications`), + link: `${match.url}/notifications`, + id: 3, + }); } - async loadOrganization() { - const { match, setBreadcrumb } = this.props; - const id = parseInt(match.params.id, 10); + let showCardHeader = true; - this.setState({ contentError: null, hasContentLoading: true }); - try { - const { data } = await OrganizationsAPI.readDetail(id); - setBreadcrumb(data); - this.setState({ organization: data }); - } catch (err) { - this.setState({ contentError: err }); - } finally { - this.setState({ hasContentLoading: false }); - } + if (location.pathname.endsWith('edit')) { + showCardHeader = false; } - render() { - const { location, match, me, i18n } = this.props; - - const { - organization, - contentError, - hasContentLoading, - isInitialized, - isNotifAdmin, - isAuditorOfThisOrg, - isAdminOfThisOrg, - } = this.state; - - const canSeeNotificationsTab = - me.is_system_auditor || isNotifAdmin || isAuditorOfThisOrg; - const canToggleNotifications = - isNotifAdmin && - (me.is_system_auditor || isAuditorOfThisOrg || isAdminOfThisOrg); - - const tabsArray = [ - { - name: ( - <> - - {i18n._(t`Back to Organizations`)} - - ), - link: `/organizations`, - id: 99, - }, - { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, - { name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 }, - { name: i18n._(t`Teams`), link: `${match.url}/teams`, id: 2 }, - ]; - - if (canSeeNotificationsTab) { - tabsArray.push({ - name: i18n._(t`Notifications`), - link: `${match.url}/notifications`, - id: 3, - }); - } - - let showCardHeader = true; - - if (!isInitialized || location.pathname.endsWith('edit')) { - showCardHeader = false; - } - - if (!hasContentLoading && contentError) { - return ( - - - - {contentError.response.status === 404 && ( - - {i18n._(t`Organization not found.`)}{' '} - - {i18n._(t`View all Organizations.`)} - - - )} - - - - ); - } - + if (!organizationLoading && organizationError) { return ( - {showCardHeader && } - - - {organization && ( - - - + + {organizationError.response.status === 404 && ( + + {i18n._(t`Organization not found.`)}{' '} + + {i18n._(t`View all Organizations.`)} + + )} - {organization && ( - - - - )} - {organization && ( - - - - )} - - - - {canSeeNotificationsTab && ( - - - - )} - - {!hasContentLoading && ( - - {match.params.id && ( - - {i18n._(t`View Organization Details`)} - - )} - - )} - - , - + ); } + + if (!rolesLoading && rolesError) { + return ( + + + + + + ); + } + + return ( + + + {showCardHeader && } + + + {organization && ( + + + + )} + {organization && ( + + + + )} + {organization && ( + + + + )} + + + + {canSeeNotificationsTab && ( + + + + )} + + {!organizationLoading && !rolesLoading && ( + + {match.params.id && ( + + {i18n._(t`View Organization Details`)} + + )} + + )} + + , + + + + ); } export default withI18n()(withRouter(Organization)); diff --git a/awx/ui_next/src/screens/Organization/Organization.test.jsx b/awx/ui_next/src/screens/Organization/Organization.test.jsx index 298ae9188e..10982505d5 100644 --- a/awx/ui_next/src/screens/Organization/Organization.test.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { OrganizationsAPI } from '../../api'; import { @@ -37,30 +38,44 @@ async function getOrganizations(params) { } describe('', () => { - test('initially renders succesfully', () => { + let wrapper; + + beforeAll(() => { OrganizationsAPI.readDetail.mockResolvedValue({ data: mockOrganization }); + OrganizationsAPI.readGalaxyCredentials.mockResolvedValue({ + data: { + results: [], + }, + }); + }); + + test('initially renders succesfully', async () => { OrganizationsAPI.read.mockImplementation(getOrganizations); - mountWithContexts( {}} me={mockMe} />); + await act(async () => { + mountWithContexts( {}} me={mockMe} />); + }); }); test('notifications tab shown for admins', async done => { - OrganizationsAPI.readDetail.mockResolvedValue({ data: mockOrganization }); OrganizationsAPI.read.mockImplementation(getOrganizations); - const wrapper = mountWithContexts( - {}} me={mockMe} /> - ); + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + }); + const tabs = await waitForElement( wrapper, '.pf-c-tabs__item', el => el.length === 5 ); expect(tabs.last().text()).toEqual('Notifications'); + wrapper.unmount(); done(); }); test('notifications tab hidden with reduced permissions', async done => { - OrganizationsAPI.readDetail.mockResolvedValue({ data: mockOrganization }); OrganizationsAPI.read.mockResolvedValue({ count: 0, next: null, @@ -68,15 +83,19 @@ describe('', () => { data: { results: [] }, }); - const wrapper = mountWithContexts( - {}} me={mockMe} /> - ); + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + }); + const tabs = await waitForElement( wrapper, '.pf-c-tabs__item', el => el.length === 4 ); tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications')); + wrapper.unmount(); done(); }); @@ -84,24 +103,27 @@ describe('', () => { const history = createMemoryHistory({ initialEntries: ['/organizations/1/foobar'], }); - const wrapper = mountWithContexts( - {}} me={mockMe} />, - { - context: { - router: { - history, - route: { - location: history.location, - match: { - params: { id: 1 }, - url: '/organizations/1/foobar', - path: '/organizations/1/foobar', + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { id: 1 }, + url: '/organizations/1/foobar', + path: '/organizations/1/foobar', + }, }, }, }, - }, - } - ); + } + ); + }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); + wrapper.unmount(); }); }); diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx index b0831829b0..6cb8c08891 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx @@ -16,9 +16,13 @@ function OrganizationAdd() { try { const { data: response } = await OrganizationsAPI.create(values); await Promise.all( - groupsToAssociate.map(id => - OrganizationsAPI.associateInstanceGroup(response.id, id) - ) + groupsToAssociate + .map(id => OrganizationsAPI.associateInstanceGroup(response.id, id)) + .concat( + values.galaxy_credentials.map(({ id: credId }) => + OrganizationsAPI.associateGalaxyCredential(response.id, credId) + ) + ) ); history.push(`/organizations/${response.id}`); } catch (error) { diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx index 1d579fb85d..ff969b86b5 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx @@ -16,11 +16,12 @@ describe('', () => { name: 'new name', description: 'new description', custom_virtualenv: 'Buzz', + galaxy_credentials: [], }; OrganizationsAPI.create.mockResolvedValueOnce({ data: {} }); await act(async () => { const wrapper = mountWithContexts(); - wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []); + wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, []); }); expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData); }); @@ -46,6 +47,7 @@ describe('', () => { name: 'new name', description: 'new description', custom_virtualenv: 'Buzz', + galaxy_credentials: [], }; OrganizationsAPI.create.mockResolvedValueOnce({ data: { @@ -62,7 +64,7 @@ describe('', () => { context: { router: { history } }, }); await waitForElement(wrapper, 'button[aria-label="Save"]'); - await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3], []); + await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3]); }); expect(history.location.pathname).toEqual('/organizations/5'); }); @@ -72,6 +74,7 @@ describe('', () => { name: 'new name', description: 'new description', custom_virtualenv: 'Buzz', + galaxy_credentials: [], }; OrganizationsAPI.create.mockResolvedValueOnce({ data: { @@ -87,10 +90,42 @@ describe('', () => { wrapper = mountWithContexts(); }); await waitForElement(wrapper, 'button[aria-label="Save"]'); - await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3], []); + await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3]); expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(5, 3); }); + test('onSubmit should post galaxy credentials', async () => { + const orgData = { + name: 'new name', + description: 'new description', + custom_virtualenv: 'Buzz', + galaxy_credentials: [ + { + id: 9000, + }, + ], + }; + OrganizationsAPI.create.mockResolvedValueOnce({ + data: { + id: 5, + related: { + instance_groups: '/api/v2/organizations/5/instance_groups', + }, + ...orgData, + }, + }); + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForElement(wrapper, 'button[aria-label="Save"]'); + await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3]); + expect(OrganizationsAPI.associateGalaxyCredential).toHaveBeenCalledWith( + 5, + 9000 + ); + }); + test('AnsibleSelect component renders if there are virtual environments', async () => { const mockInstanceGroups = [ { name: 'One', id: 1 }, diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx index 522607e6d7..2054c18c52 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx @@ -12,6 +12,7 @@ import { import { CardBody, CardActionsRow } from '../../../components/Card'; import AlertModal from '../../../components/AlertModal'; import ChipGroup from '../../../components/ChipGroup'; +import CredentialChip from '../../../components/CredentialChip'; import ContentError from '../../../components/ContentError'; import ContentLoading from '../../../components/ContentLoading'; import DeleteButton from '../../../components/DeleteButton'; @@ -30,6 +31,7 @@ function OrganizationDetail({ i18n, organization }) { created, modified, summary_fields, + galaxy_credentials, } = organization; const [contentError, setContentError] = useState(null); const [hasContentLoading, setHasContentLoading] = useState(true); @@ -113,6 +115,23 @@ function OrganizationDetail({ i18n, organization }) { } /> )} + {galaxy_credentials && galaxy_credentials.length > 0 && ( + + {galaxy_credentials.map(credential => ( + + ))} + + } + /> + )} {summary_fields.user_capabilities.edit && ( diff --git a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx index c95c47faf2..400d3758c8 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx @@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; import { OrganizationsAPI } from '../../../api'; import { Config } from '../../../contexts/Config'; - +import { getAddedAndRemoved } from '../../../util/lists'; import OrganizationForm from '../shared/OrganizationForm'; function OrganizationEdit({ organization }) { @@ -18,16 +18,39 @@ function OrganizationEdit({ organization }) { groupsToDisassociate ) => { try { + const { + added: addedCredentials, + removed: removedCredentials, + } = getAddedAndRemoved( + organization.galaxy_credentials, + values.galaxy_credentials + ); + + const addedCredentialIds = addedCredentials.map(({ id }) => id); + const removedCredentialIds = removedCredentials.map(({ id }) => id); + await OrganizationsAPI.update(organization.id, values); await Promise.all( - groupsToAssociate.map(id => - OrganizationsAPI.associateInstanceGroup(organization.id, id) - ) + groupsToAssociate + .map(id => + OrganizationsAPI.associateInstanceGroup(organization.id, id) + ) + .concat( + addedCredentialIds.map(id => + OrganizationsAPI.associateGalaxyCredential(organization.id, id) + ) + ) ); await Promise.all( - groupsToDisassociate.map(id => - OrganizationsAPI.disassociateInstanceGroup(organization.id, id) - ) + groupsToDisassociate + .map(id => + OrganizationsAPI.disassociateInstanceGroup(organization.id, id) + ) + .concat( + removedCredentialIds.map(id => + OrganizationsAPI.disassociateGalaxyCredential(organization.id, id) + ) + ) ); history.push(detailsUrl); } catch (error) { diff --git a/awx/ui_next/src/screens/Organization/Organizations.jsx b/awx/ui_next/src/screens/Organization/Organizations.jsx index 2a289b6be6..8ed5f21f1d 100644 --- a/awx/ui_next/src/screens/Organization/Organizations.jsx +++ b/awx/ui_next/src/screens/Organization/Organizations.jsx @@ -48,7 +48,7 @@ class Organizations extends Component { }; render() { - const { match, history, location } = this.props; + const { match } = this.props; const { breadcrumbConfig } = this.state; return ( @@ -62,8 +62,6 @@ class Organizations extends Component { {({ me }) => ( diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx index f164b19a6e..cba0d62224 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx @@ -1,6 +1,6 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Formik, useField } from 'formik'; +import { Formik, useField, useFormikContext } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Form, FormGroup } from '@patternfly/react-core'; @@ -16,6 +16,7 @@ import { InstanceGroupsLookup } from '../../../components/Lookup'; import { getAddedAndRemoved } from '../../../util/lists'; import { required, minMaxValue } from '../../../util/validators'; import { FormColumnLayout } from '../../../components/FormLayout'; +import CredentialLookup from '../../../components/Lookup/CredentialLookup'; function OrganizationFormFields({ i18n, @@ -23,8 +24,15 @@ function OrganizationFormFields({ instanceGroups, setInstanceGroups, }) { + const { setFieldValue } = useFormikContext(); const [venvField] = useField('custom_virtualenv'); + const [ + galaxyCredentialsField, + galaxyCredentialsMeta, + galaxyCredentialsHelpers, + ] = useField('galaxy_credentials'); + const defaultVenv = { label: i18n._(t`Use Default Ansible Environment`), value: '/venv/ansible/', @@ -32,6 +40,13 @@ function OrganizationFormFields({ }; const { custom_virtualenvs } = useContext(ConfigContext); + const handleCredentialUpdate = useCallback( + value => { + setFieldValue('galaxy_credentials', value); + }, + [setFieldValue] + ); + return ( <> + galaxyCredentialsHelpers.setTouched()} + onChange={handleCredentialUpdate} + value={galaxyCredentialsField.value} + multiple + /> ); } @@ -160,6 +185,7 @@ function OrganizationForm({ description: organization.description, custom_virtualenv: organization.custom_virtualenv || '', max_hosts: organization.max_hosts || '0', + galaxy_credentials: organization.galaxy_credentials || [], }} onSubmit={handleSubmit} > diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx index 0048a13b13..004c7d1577 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx @@ -163,6 +163,7 @@ describe('', () => { expect(onSubmit.mock.calls[0][0]).toEqual({ name: 'new foo', description: 'new bar', + galaxy_credentials: [], custom_virtualenv: 'Fizz', max_hosts: 134, }); @@ -211,6 +212,7 @@ describe('', () => { const mockDataForm = { name: 'Foo', description: 'Bar', + galaxy_credentials: [], max_hosts: 1, custom_virtualenv: 'Fizz', }; @@ -315,6 +317,7 @@ describe('', () => { { name: 'Foo', description: 'Bar', + galaxy_credentials: [], max_hosts: 0, custom_virtualenv: 'Fizz', },