diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
new file mode 100644
index 0000000000..4d5cf86cb0
--- /dev/null
+++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { withI18n } from '@lingui/react';
+import { bool, func, number, string } from 'prop-types';
+import { CredentialsAPI } from '@api';
+import { Credential } from '@types';
+import { mergeParams } from '@util/qs';
+import { FormGroup } from '@patternfly/react-core';
+import Lookup from '@components/Lookup';
+
+function CredentialLookup({
+ helperTextInvalid,
+ label,
+ isValid,
+ onBlur,
+ onChange,
+ required,
+ credentialTypeId,
+ value,
+}) {
+ const getCredentials = async params =>
+ CredentialsAPI.read(
+ mergeParams(params, { credential_type: credentialTypeId })
+ );
+
+ return (
+
+
+
+ );
+}
+
+CredentialLookup.propTypes = {
+ credentialTypeId: number.isRequired,
+ helperTextInvalid: string,
+ isValid: bool,
+ label: string.isRequired,
+ onBlur: func,
+ onChange: func.isRequired,
+ required: bool,
+ value: Credential,
+};
+
+CredentialLookup.defaultProps = {
+ helperTextInvalid: '',
+ isValid: true,
+ onBlur: () => {},
+ required: false,
+ value: null,
+};
+
+export { CredentialLookup as _CredentialLookup };
+export default withI18n()(CredentialLookup);
diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx
new file mode 100644
index 0000000000..797cbdf6c2
--- /dev/null
+++ b/awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import CredentialLookup, { _CredentialLookup } from './CredentialLookup';
+import { CredentialsAPI } from '@api';
+
+jest.mock('@api');
+
+describe('CredentialLookup', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mountWithContexts(
+ {}} />
+ );
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('initially renders successfully', () => {
+ expect(wrapper.find('CredentialLookup')).toHaveLength(1);
+ });
+ test('should fetch credentials', () => {
+ expect(CredentialsAPI.read).toHaveBeenCalledTimes(1);
+ expect(CredentialsAPI.read).toHaveBeenCalledWith({
+ credential_type: 1,
+ order_by: 'name',
+ page: 1,
+ page_size: 5,
+ });
+ });
+ test('should display label', () => {
+ const title = wrapper.find('FormGroup .pf-c-form__label-text');
+ expect(title.text()).toEqual('Foo');
+ });
+ test('should define default value for function props', () => {
+ expect(_CredentialLookup.defaultProps.onBlur).toBeInstanceOf(Function);
+ expect(_CredentialLookup.defaultProps.onBlur).not.toThrow();
+ });
+});
diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx
new file mode 100644
index 0000000000..8efb43b091
--- /dev/null
+++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { string, func, bool } from 'prop-types';
+import { OrganizationsAPI } from '@api';
+import { Organization } from '@types';
+import { FormGroup } from '@patternfly/react-core';
+import Lookup from '@components/Lookup';
+
+const getOrganizations = async params => OrganizationsAPI.read(params);
+
+function OrganizationLookup({
+ helperTextInvalid,
+ i18n,
+ isValid,
+ onBlur,
+ onChange,
+ required,
+ value,
+}) {
+ return (
+
+
+
+ );
+}
+
+OrganizationLookup.propTypes = {
+ helperTextInvalid: string,
+ isValid: bool,
+ onBlur: func,
+ onChange: func.isRequired,
+ required: bool,
+ value: Organization,
+};
+
+OrganizationLookup.defaultProps = {
+ helperTextInvalid: '',
+ isValid: true,
+ onBlur: () => {},
+ required: false,
+ value: null,
+};
+
+export default withI18n()(OrganizationLookup);
+export { OrganizationLookup as _OrganizationLookup };
diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.test.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.test.jsx
new file mode 100644
index 0000000000..fef9a90281
--- /dev/null
+++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.test.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import OrganizationLookup, { _OrganizationLookup } from './OrganizationLookup';
+import { OrganizationsAPI } from '@api';
+
+jest.mock('@api');
+
+describe('OrganizationLookup', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mountWithContexts( {}} />);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('initially renders successfully', () => {
+ expect(wrapper).toHaveLength(1);
+ });
+ test('should fetch organizations', () => {
+ expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1);
+ expect(OrganizationsAPI.read).toHaveBeenCalledWith({
+ order_by: 'name',
+ page: 1,
+ page_size: 5,
+ });
+ });
+ test('should display "Organization" label', () => {
+ const title = wrapper.find('FormGroup .pf-c-form__label-text');
+ expect(title.text()).toEqual('Organization');
+ });
+ test('should define default value for function props', () => {
+ expect(_OrganizationLookup.defaultProps.onBlur).toBeInstanceOf(Function);
+ expect(_OrganizationLookup.defaultProps.onBlur).not.toThrow();
+ });
+});