Merge pull request #8281 from mabashian/7835-galaxy-cred-org-2

Add Galaxy Credentials field to organizations form

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-10-20 15:37:08 +00:00 committed by GitHub
commit 677fb594e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 400 additions and 235 deletions

View File

@ -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;

View File

@ -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 }) => (
<OptionsList
value={state.selectedItems}
@ -154,6 +164,7 @@ function CredentialLookup({
name="credential"
selectItem={item => 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,

View File

@ -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: (
<>
<CaretLeftIcon />
{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: (
<>
<CaretLeftIcon />
{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 (
<PageSection>
<Card>
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(t`Organization not found.`)}{' '}
<Link to="/organizations">
{i18n._(t`View all Organizations.`)}
</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
if (!organizationLoading && organizationError) {
return (
<PageSection>
<Card>
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
<Switch>
<Redirect
from="/organizations/:id"
to="/organizations/:id/details"
exact
/>
{organization && (
<Route path="/organizations/:id/edit">
<OrganizationEdit organization={organization} />
</Route>
<ContentError error={organizationError}>
{organizationError.response.status === 404 && (
<span>
{i18n._(t`Organization not found.`)}{' '}
<Link to="/organizations">
{i18n._(t`View all Organizations.`)}
</Link>
</span>
)}
{organization && (
<Route path="/organizations/:id/details">
<OrganizationDetail organization={organization} />
</Route>
)}
{organization && (
<Route path="/organizations/:id/access">
<ResourceAccessList
resource={organization}
apiModel={OrganizationsAPI}
/>
</Route>
)}
<Route path="/organizations/:id/teams">
<OrganizationTeams id={Number(match.params.id)} />
</Route>
{canSeeNotificationsTab && (
<Route path="/organizations/:id/notifications">
<NotificationList
id={Number(match.params.id)}
canToggleNotifications={canToggleNotifications}
apiModel={OrganizationsAPI}
showApprovalsToggle
/>
</Route>
)}
<Route key="not-found" path="*">
{!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link to={`/organizations/${match.params.id}/details`}>
{i18n._(t`View Organization Details`)}
</Link>
)}
</ContentError>
)}
</Route>
,
</Switch>
</ContentError>
</Card>
</PageSection>
);
}
if (!rolesLoading && rolesError) {
return (
<PageSection>
<Card>
<ContentError error={rolesError} />
</Card>
</PageSection>
);
}
return (
<PageSection>
<Card>
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
<Switch>
<Redirect
from="/organizations/:id"
to="/organizations/:id/details"
exact
/>
{organization && (
<Route path="/organizations/:id/edit">
<OrganizationEdit organization={organization} />
</Route>
)}
{organization && (
<Route path="/organizations/:id/details">
<OrganizationDetail organization={organization} />
</Route>
)}
{organization && (
<Route path="/organizations/:id/access">
<ResourceAccessList
resource={organization}
apiModel={OrganizationsAPI}
/>
</Route>
)}
<Route path="/organizations/:id/teams">
<OrganizationTeams id={Number(match.params.id)} />
</Route>
{canSeeNotificationsTab && (
<Route path="/organizations/:id/notifications">
<NotificationList
id={Number(match.params.id)}
canToggleNotifications={canToggleNotifications}
apiModel={OrganizationsAPI}
showApprovalsToggle
/>
</Route>
)}
<Route key="not-found" path="*">
{!organizationLoading && !rolesLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link to={`/organizations/${match.params.id}/details`}>
{i18n._(t`View Organization Details`)}
</Link>
)}
</ContentError>
)}
</Route>
,
</Switch>
</Card>
</PageSection>
);
}
export default withI18n()(withRouter(Organization));

View File

@ -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('<Organization />', () => {
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(<Organization setBreadcrumb={() => {}} me={mockMe} />);
await act(async () => {
mountWithContexts(<Organization setBreadcrumb={() => {}} me={mockMe} />);
});
});
test('notifications tab shown for admins', async done => {
OrganizationsAPI.readDetail.mockResolvedValue({ data: mockOrganization });
OrganizationsAPI.read.mockImplementation(getOrganizations);
const wrapper = mountWithContexts(
<Organization setBreadcrumb={() => {}} me={mockMe} />
);
await act(async () => {
wrapper = mountWithContexts(
<Organization setBreadcrumb={() => {}} 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('<Organization />', () => {
data: { results: [] },
});
const wrapper = mountWithContexts(
<Organization setBreadcrumb={() => {}} me={mockMe} />
);
await act(async () => {
wrapper = mountWithContexts(
<Organization setBreadcrumb={() => {}} 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('<Organization />', () => {
const history = createMemoryHistory({
initialEntries: ['/organizations/1/foobar'],
});
const wrapper = mountWithContexts(
<Organization setBreadcrumb={() => {}} 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(
<Organization setBreadcrumb={() => {}} 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();
});
});

View File

@ -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) {

View File

@ -16,11 +16,12 @@ describe('<OrganizationAdd />', () => {
name: 'new name',
description: 'new description',
custom_virtualenv: 'Buzz',
galaxy_credentials: [],
};
OrganizationsAPI.create.mockResolvedValueOnce({ data: {} });
await act(async () => {
const wrapper = mountWithContexts(<OrganizationAdd />);
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, []);
});
expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData);
});
@ -46,6 +47,7 @@ describe('<OrganizationAdd />', () => {
name: 'new name',
description: 'new description',
custom_virtualenv: 'Buzz',
galaxy_credentials: [],
};
OrganizationsAPI.create.mockResolvedValueOnce({
data: {
@ -62,7 +64,7 @@ describe('<OrganizationAdd />', () => {
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('<OrganizationAdd />', () => {
name: 'new name',
description: 'new description',
custom_virtualenv: 'Buzz',
galaxy_credentials: [],
};
OrganizationsAPI.create.mockResolvedValueOnce({
data: {
@ -87,10 +90,42 @@ describe('<OrganizationAdd />', () => {
wrapper = mountWithContexts(<OrganizationAdd />);
});
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(<OrganizationAdd />);
});
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 },

View File

@ -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 && (
<Detail
fullWidth
label={i18n._(t`Galaxy Credentials`)}
value={
<ChipGroup numChips={5} totalChips={galaxy_credentials.length}>
{galaxy_credentials.map(credential => (
<CredentialChip
credential={credential}
key={credential.id}
isReadOnly
/>
))}
</ChipGroup>
}
/>
)}
</DetailList>
<CardActionsRow>
{summary_fields.user_capabilities.edit && (

View File

@ -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) {

View File

@ -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 {
<Config>
{({ me }) => (
<Organization
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
me={me || {}}
/>

View File

@ -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 (
<>
<FormField
@ -86,6 +101,16 @@ function OrganizationFormFields({
t`Select the Instance Groups for this Organization to run on.`
)}
/>
<CredentialLookup
credentialTypeNamespace="galaxy_api_token"
label={i18n._(t`Galaxy Credentials`)}
helperTextInvalid={galaxyCredentialsMeta.error}
isValid={!galaxyCredentialsMeta.touched || !galaxyCredentialsMeta.error}
onBlur={() => 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}
>

View File

@ -163,6 +163,7 @@ describe('<OrganizationForm />', () => {
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('<OrganizationForm />', () => {
const mockDataForm = {
name: 'Foo',
description: 'Bar',
galaxy_credentials: [],
max_hosts: 1,
custom_virtualenv: 'Fizz',
};
@ -315,6 +317,7 @@ describe('<OrganizationForm />', () => {
{
name: 'Foo',
description: 'Bar',
galaxy_credentials: [],
max_hosts: 0,
custom_virtualenv: 'Fizz',
},