diff --git a/awx/ui_next/src/api/models/Credentials.js b/awx/ui_next/src/api/models/Credentials.js
index 2e3634b4e5..9b31506956 100644
--- a/awx/ui_next/src/api/models/Credentials.js
+++ b/awx/ui_next/src/api/models/Credentials.js
@@ -4,6 +4,14 @@ class Credentials extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/credentials/';
+
+ this.readAccessList = this.readAccessList.bind(this);
+ }
+
+ readAccessList(id, params) {
+ return this.http.get(`${this.baseUrl}${id}/access_list/`, {
+ params,
+ });
}
}
diff --git a/awx/ui_next/src/screens/Credential/Credential.jsx b/awx/ui_next/src/screens/Credential/Credential.jsx
index 31ede881cc..d7f0754d97 100644
--- a/awx/ui_next/src/screens/Credential/Credential.jsx
+++ b/awx/ui_next/src/screens/Credential/Credential.jsx
@@ -17,6 +17,7 @@ import { ResourceAccessList } from '@components/ResourceAccessList';
import ContentError from '@components/ContentError';
import RoutedTabs from '@components/RoutedTabs';
import CredentialDetail from './CredentialDetail';
+import CredentialEdit from './CredentialEdit';
import { CredentialsAPI } from '@api';
function Credential({ i18n, setBreadcrumb }) {
@@ -100,6 +101,11 @@ function Credential({ i18n, setBreadcrumb }) {
path="/credentials/:id/details"
render={() => }
/>,
+ }
+ />,
credential.organization && (
{i18n._(`View Credential Details`)}
- )}
+ )}
)
}
diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
index 964b301d8a..f72c33943d 100644
--- a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
@@ -1,14 +1,84 @@
-import React from 'react';
-import { Card, CardBody, PageSection } from '@patternfly/react-core';
+import React, { useState, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { PageSection, Card } from '@patternfly/react-core';
+import { CardBody } from '@components/Card';
+import ContentError from '@components/ContentError';
+import ContentLoading from '@components/ContentLoading';
-function CredentialAdd() {
+import { CredentialTypesAPI, CredentialsAPI } from '@api';
+import CredentialForm from '../shared/CredentialForm';
+
+function CredentialAdd({ me }) {
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [credentialTypes, setCredentialTypes] = useState(null);
+ const history = useHistory();
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const {
+ data: { results: loadedCredentialTypes },
+ } = await CredentialTypesAPI.read({ or__kind: ['scm', 'ssh'] });
+ setCredentialTypes(loadedCredentialTypes);
+ } catch (err) {
+ setError(err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadData();
+ }, []);
+
+ const handleCancel = () => {
+ history.push('/credentials');
+ };
+
+ const handleSubmit = async values => {
+ const { organization, ...remainingValues } = values;
+ try {
+ const {
+ data: { id: credentialId },
+ } = await CredentialsAPI.create({
+ user: (me && me.id) || null,
+ organization: (organization && organization.id) || null,
+ ...remainingValues,
+ });
+ const url = `/credentials/${credentialId}/details`;
+ history.push(`${url}`);
+ } catch (err) {
+ setError(err);
+ }
+ };
+
+ if (error) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ if (isLoading) {
+ return ;
+ }
return (
- Coming soon :)
+
+
+
);
}
+export { CredentialAdd as _CredentialAdd };
export default CredentialAdd;
diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.test.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.test.jsx
new file mode 100644
index 0000000000..4d1abe0672
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.test.jsx
@@ -0,0 +1,201 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { sleep } from '@testUtils/testUtils';
+
+import { CredentialsAPI, CredentialTypesAPI } from '@api';
+import CredentialAdd from './CredentialAdd';
+
+jest.mock('@api');
+
+CredentialTypesAPI.read.mockResolvedValue({
+ data: {
+ results: [
+ {
+ id: 2,
+ type: 'credential_type',
+ url: '/api/v2/credential_types/2/',
+ related: {
+ credentials: '/api/v2/credential_types/2/credentials/',
+ activity_stream: '/api/v2/credential_types/2/activity_stream/',
+ },
+ summary_fields: {
+ user_capabilities: {
+ edit: false,
+ delete: false,
+ },
+ },
+ created: '2020-02-12T19:42:43.551238Z',
+ modified: '2020-02-12T19:43:03.164800Z',
+ name: 'Source Control',
+ description: '',
+ kind: 'scm',
+ namespace: 'scm',
+ managed_by_tower: true,
+ inputs: {
+ fields: [
+ {
+ id: 'username',
+ label: 'Username',
+ type: 'string',
+ },
+ {
+ id: 'password',
+ label: 'Password',
+ type: 'string',
+ secret: true,
+ },
+ {
+ id: 'ssh_key_data',
+ label: 'SCM Private Key',
+ type: 'string',
+ format: 'ssh_private_key',
+ secret: true,
+ multiline: true,
+ },
+ {
+ id: 'ssh_key_unlock',
+ label: 'Private Key Passphrase',
+ type: 'string',
+ secret: true,
+ },
+ ],
+ },
+ injectors: {},
+ },
+ {
+ id: 1,
+ type: 'credential_type',
+ url: '/api/v2/credential_types/1/',
+ related: {
+ credentials: '/api/v2/credential_types/1/credentials/',
+ activity_stream: '/api/v2/credential_types/1/activity_stream/',
+ },
+ summary_fields: {
+ user_capabilities: {
+ edit: false,
+ delete: false,
+ },
+ },
+ created: '2020-02-12T19:42:43.539626Z',
+ modified: '2020-02-12T19:43:03.159739Z',
+ name: 'Machine',
+ description: '',
+ kind: 'ssh',
+ namespace: 'ssh',
+ managed_by_tower: true,
+ inputs: {
+ fields: [
+ {
+ id: 'username',
+ label: 'Username',
+ type: 'string',
+ },
+ {
+ id: 'password',
+ label: 'Password',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ {
+ id: 'ssh_key_data',
+ label: 'SSH Private Key',
+ type: 'string',
+ format: 'ssh_private_key',
+ secret: true,
+ multiline: true,
+ },
+ {
+ id: 'ssh_public_key_data',
+ label: 'Signed SSH Certificate',
+ type: 'string',
+ multiline: true,
+ secret: true,
+ },
+ {
+ id: 'ssh_key_unlock',
+ label: 'Private Key Passphrase',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ {
+ id: 'become_method',
+ label: 'Privilege Escalation Method',
+ type: 'string',
+ help_text:
+ 'Specify a method for "become" operations. This is equivalent to specifying the --become-method Ansible parameter.',
+ },
+ {
+ id: 'become_username',
+ label: 'Privilege Escalation Username',
+ type: 'string',
+ },
+ {
+ id: 'become_password',
+ label: 'Privilege Escalation Password',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ ],
+ },
+ injectors: {},
+ },
+ ],
+ },
+});
+
+CredentialsAPI.create.mockResolvedValue({ data: { id: 13 } });
+
+describe('', () => {
+ let wrapper;
+ let history;
+
+ beforeEach(async () => {
+ history = createMemoryHistory({ initialEntries: ['/credentials'] });
+ await act(async () => {
+ wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ });
+ });
+ });
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('Initially renders successfully', () => {
+ expect(wrapper.length).toBe(1);
+ });
+ test('handleSubmit should call the api and redirect to details page', async () => {
+ await waitForElement(wrapper, 'isLoading', el => el.length === 0);
+
+ wrapper.find('CredentialForm').prop('onSubmit')({
+ user: 1,
+ organization: null,
+ name: 'foo',
+ description: 'bar',
+ credential_type: '2',
+ inputs: {},
+ });
+ await sleep(1);
+ expect(CredentialsAPI.create).toHaveBeenCalledWith({
+ user: 1,
+ organization: null,
+ name: 'foo',
+ description: 'bar',
+ credential_type: '2',
+ inputs: {},
+ });
+ expect(history.location.pathname).toBe('/credentials/13/details');
+ });
+
+ test('handleCancel should return the user back to the inventories list', async () => {
+ await waitForElement(wrapper, 'isLoading', el => el.length === 0);
+ wrapper.find('Button[aria-label="Cancel"]').simulate('click');
+ expect(history.location.pathname).toEqual('/credentials');
+ });
+});
diff --git a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
index 290275fb15..1e35f7d36c 100644
--- a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
@@ -82,7 +82,7 @@ function CredentialDetail({ i18n, credential }) {
/>
);
} else if (secret === true) {
- detail = ;
+ detail = null;
} else {
detail = ;
}
diff --git a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx
index c1f34c16a7..1411eb70fa 100644
--- a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.test.jsx
@@ -49,10 +49,6 @@ describe('', () => {
mockCredential.summary_fields.credential_type.name
);
expectDetailToMatch(wrapper, 'Username', mockCredential.inputs.username);
- expectDetailToMatch(wrapper, 'Password', 'Encrypted');
- expectDetailToMatch(wrapper, 'SSH Private Key', 'Encrypted');
- expectDetailToMatch(wrapper, 'Signed SSH Certificate', 'Encrypted');
- expectDetailToMatch(wrapper, 'Private Key Passphrase', 'Encrypted');
expectDetailToMatch(
wrapper,
'Privilege Escalation Method',
diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx
new file mode 100644
index 0000000000..e6086f992e
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx
@@ -0,0 +1,81 @@
+import React, { useState, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { object } from 'prop-types';
+
+import { CardBody } from '@components/Card';
+import { CredentialsAPI, CredentialTypesAPI } from '@api';
+import ContentError from '@components/ContentError';
+import ContentLoading from '@components/ContentLoading';
+import CredentialForm from '../shared/CredentialForm';
+
+function CredentialEdit({ credential, me }) {
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [credentialTypes, setCredentialTypes] = useState(null);
+ const history = useHistory();
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const {
+ data: { results: loadedCredentialTypes },
+ } = await CredentialTypesAPI.read({ or__kind: ['scm', 'ssh'] });
+ setCredentialTypes(loadedCredentialTypes);
+ } catch (err) {
+ setError(err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadData();
+ }, []);
+
+ const handleCancel = () => {
+ const url = `/credentials/${credential.id}/details`;
+
+ history.push(`${url}`);
+ };
+
+ const handleSubmit = async values => {
+ const { organization, ...remainingValues } = values;
+ try {
+ const {
+ data: { id: credentialId },
+ } = await CredentialsAPI.update(credential.id, {
+ user: (me && me.id) || null,
+ organization: (organization && organization.id) || null,
+ ...remainingValues,
+ });
+ const url = `/credentials/${credentialId}/details`;
+ history.push(`${url}`);
+ } catch (err) {
+ setError(err);
+ }
+ };
+
+ if (error) {
+ return ;
+ }
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+}
+
+CredentialEdit.proptype = {
+ inventory: object.isRequired,
+};
+
+export { CredentialEdit as _CredentialEdit };
+export default CredentialEdit;
diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx
new file mode 100644
index 0000000000..8a0cbe13d7
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx
@@ -0,0 +1,298 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { sleep } from '@testUtils/testUtils';
+
+import { CredentialsAPI, CredentialTypesAPI } from '@api';
+import CredentialEdit from './CredentialEdit';
+
+jest.mock('@api');
+
+const mockCredential = {
+ id: 3,
+ type: 'credential',
+ url: '/api/v2/credentials/3/',
+ related: {
+ named_url: '/api/v2/credentials/oersdgfasf++Machine+ssh++org/',
+ created_by: '/api/v2/users/1/',
+ modified_by: '/api/v2/users/1/',
+ organization: '/api/v2/organizations/1/',
+ activity_stream: '/api/v2/credentials/3/activity_stream/',
+ access_list: '/api/v2/credentials/3/access_list/',
+ object_roles: '/api/v2/credentials/3/object_roles/',
+ owner_users: '/api/v2/credentials/3/owner_users/',
+ owner_teams: '/api/v2/credentials/3/owner_teams/',
+ copy: '/api/v2/credentials/3/copy/',
+ input_sources: '/api/v2/credentials/3/input_sources/',
+ credential_type: '/api/v2/credential_types/1/',
+ },
+ summary_fields: {
+ organization: {
+ id: 1,
+ name: 'org',
+ description: '',
+ },
+ credential_type: {
+ id: 1,
+ name: 'Machine',
+ description: '',
+ },
+ created_by: {
+ id: 1,
+ username: 'admin',
+ first_name: '',
+ last_name: '',
+ },
+ modified_by: {
+ id: 1,
+ username: 'admin',
+ first_name: '',
+ last_name: '',
+ },
+ object_roles: {
+ admin_role: {
+ description: 'Can manage all aspects of the credential',
+ name: 'Admin',
+ id: 36,
+ },
+ use_role: {
+ description: 'Can use the credential in a job template',
+ name: 'Use',
+ id: 37,
+ },
+ read_role: {
+ description: 'May view settings for the credential',
+ name: 'Read',
+ id: 38,
+ },
+ },
+ user_capabilities: {
+ edit: true,
+ delete: true,
+ copy: true,
+ use: true,
+ },
+ owners: [
+ {
+ id: 1,
+ type: 'user',
+ name: 'admin',
+ description: ' ',
+ url: '/api/v2/users/1/',
+ },
+ {
+ id: 1,
+ type: 'organization',
+ name: 'org',
+ description: '',
+ url: '/api/v2/organizations/1/',
+ },
+ ],
+ },
+ created: '2020-02-18T15:35:04.563928Z',
+ modified: '2020-02-18T15:35:04.563957Z',
+ name: 'oersdgfasf',
+ description: '',
+ organization: 1,
+ credential_type: 1,
+ inputs: {},
+ kind: 'ssh',
+ cloud: false,
+ kubernetes: false,
+};
+
+CredentialTypesAPI.read.mockResolvedValue({
+ data: {
+ results: [
+ {
+ id: 2,
+ type: 'credential_type',
+ url: '/api/v2/credential_types/2/',
+ related: {
+ credentials: '/api/v2/credential_types/2/credentials/',
+ activity_stream: '/api/v2/credential_types/2/activity_stream/',
+ },
+ summary_fields: {
+ user_capabilities: {
+ edit: false,
+ delete: false,
+ },
+ },
+ created: '2020-02-12T19:42:43.551238Z',
+ modified: '2020-02-12T19:43:03.164800Z',
+ name: 'Source Control',
+ description: '',
+ kind: 'scm',
+ namespace: 'scm',
+ managed_by_tower: true,
+ inputs: {
+ fields: [
+ {
+ id: 'username',
+ label: 'Username',
+ type: 'string',
+ },
+ {
+ id: 'password',
+ label: 'Password',
+ type: 'string',
+ secret: true,
+ },
+ {
+ id: 'ssh_key_data',
+ label: 'SCM Private Key',
+ type: 'string',
+ format: 'ssh_private_key',
+ secret: true,
+ multiline: true,
+ },
+ {
+ id: 'ssh_key_unlock',
+ label: 'Private Key Passphrase',
+ type: 'string',
+ secret: true,
+ },
+ ],
+ },
+ injectors: {},
+ },
+ {
+ id: 1,
+ type: 'credential_type',
+ url: '/api/v2/credential_types/1/',
+ related: {
+ credentials: '/api/v2/credential_types/1/credentials/',
+ activity_stream: '/api/v2/credential_types/1/activity_stream/',
+ },
+ summary_fields: {
+ user_capabilities: {
+ edit: false,
+ delete: false,
+ },
+ },
+ created: '2020-02-12T19:42:43.539626Z',
+ modified: '2020-02-12T19:43:03.159739Z',
+ name: 'Machine',
+ description: '',
+ kind: 'ssh',
+ namespace: 'ssh',
+ managed_by_tower: true,
+ inputs: {
+ fields: [
+ {
+ id: 'username',
+ label: 'Username',
+ type: 'string',
+ },
+ {
+ id: 'password',
+ label: 'Password',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ {
+ id: 'ssh_key_data',
+ label: 'SSH Private Key',
+ type: 'string',
+ format: 'ssh_private_key',
+ secret: true,
+ multiline: true,
+ },
+ {
+ id: 'ssh_public_key_data',
+ label: 'Signed SSH Certificate',
+ type: 'string',
+ multiline: true,
+ secret: true,
+ },
+ {
+ id: 'ssh_key_unlock',
+ label: 'Private Key Passphrase',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ {
+ id: 'become_method',
+ label: 'Privilege Escalation Method',
+ type: 'string',
+ help_text:
+ 'Specify a method for "become" operations. This is equivalent to specifying the --become-method Ansible parameter.',
+ },
+ {
+ id: 'become_username',
+ label: 'Privilege Escalation Username',
+ type: 'string',
+ },
+ {
+ id: 'become_password',
+ label: 'Privilege Escalation Password',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ ],
+ },
+ injectors: {},
+ },
+ ],
+ },
+});
+
+CredentialsAPI.update.mockResolvedValue({ data: { id: 3 } });
+
+describe('', () => {
+ let wrapper;
+ let history;
+
+ beforeEach(async () => {
+ history = createMemoryHistory({ initialEntries: ['/credentials'] });
+ await act(async () => {
+ wrapper = mountWithContexts(
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
+ });
+ });
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('initially renders successfully', async () => {
+ expect(wrapper.find('CredentialEdit').length).toBe(1);
+ });
+
+ test('handleCancel returns the user to credential detail', async () => {
+ await waitForElement(wrapper, 'isLoading', el => el.length === 0);
+ wrapper.find('Button[aria-label="Cancel"]').simulate('click');
+ expect(history.location.pathname).toEqual('/credentials/3/details');
+ });
+
+ test('handleSubmit should post to the api', async () => {
+ await waitForElement(wrapper, 'isLoading', el => el.length === 0);
+
+ wrapper.find('CredentialForm').prop('onSubmit')({
+ user: 1,
+ organization: null,
+ name: 'foo',
+ description: 'bar',
+ credential_type: '2',
+ inputs: {},
+ });
+ await sleep(1);
+ expect(CredentialsAPI.update).toHaveBeenCalledWith(3, {
+ user: 1,
+ organization: null,
+ name: 'foo',
+ description: 'bar',
+ credential_type: '2',
+ inputs: {},
+ });
+ expect(history.location.pathname).toBe('/credentials/3/details');
+ });
+});
diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/index.js b/awx/ui_next/src/screens/Credential/CredentialEdit/index.js
new file mode 100644
index 0000000000..9887874371
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialEdit/index.js
@@ -0,0 +1 @@
+export { default } from './CredentialEdit';
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx
new file mode 100644
index 0000000000..89ffd6c228
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx
@@ -0,0 +1,183 @@
+import React from 'react';
+import { Formik, useField } from 'formik';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { func, shape } from 'prop-types';
+import { Form, FormGroup, Title } from '@patternfly/react-core';
+import FormField from '@components/FormField';
+import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
+import AnsibleSelect from '@components/AnsibleSelect';
+import { required } from '@util/validators';
+import OrganizationLookup from '@components/Lookup/OrganizationLookup';
+import { FormColumnLayout, SubFormLayout } from '@components/FormLayout';
+import { ManualSubForm, SourceControlSubForm } from './CredentialSubForms';
+
+function CredentialFormFields({
+ i18n,
+ credentialTypes,
+ formik,
+ initialValues,
+}) {
+ const [orgField, orgMeta, orgHelpers] = useField('organization');
+ const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
+ name: 'credential_type',
+ validate: required(i18n._(t`Select a value for this field`), i18n),
+ });
+
+ const credentialTypeOptions = Object.keys(credentialTypes).map(key => {
+ return {
+ value: credentialTypes[key].id,
+ key: credentialTypes[key].kind,
+ label: credentialTypes[key].name,
+ };
+ });
+ const scmCredentialTypeId = Object.keys(credentialTypes)
+ .filter(key => credentialTypes[key].kind === 'scm')
+ .map(key => credentialTypes[key].id)[0];
+ const sshCredentialTypeId = Object.keys(credentialTypes)
+ .filter(key => credentialTypes[key].kind === 'ssh')
+ .map(key => credentialTypes[key].id)[0];
+
+ const resetSubFormFields = (value, form) => {
+ Object.keys(form.initialValues.inputs).forEach(label => {
+ if (parseInt(value, 10) === form.initialValues.credential_type) {
+ form.setFieldValue(`inputs.${label}`, initialValues.inputs[label]);
+ } else {
+ form.setFieldValue(`inputs.${label}`, undefined);
+ }
+ form.setFieldTouched(`inputs.${label}`, false);
+ });
+ };
+
+ return (
+ <>
+
+
+ orgHelpers.setTouched()}
+ onChange={value => {
+ orgHelpers.setValue(value);
+ }}
+ value={orgField.value}
+ touched={orgMeta.touched}
+ error={orgMeta.error}
+ />
+
+ {
+ credTypeHelpers.setValue(value);
+ resetSubFormFields(value, formik);
+ }}
+ />
+
+ {formik.values.credential_type !== undefined &&
+ formik.values.credential_type !== '' && (
+
+ {i18n._(t`Type Details`)}
+ {
+ {
+ [sshCredentialTypeId]: ,
+ [scmCredentialTypeId]: ,
+ }[formik.values.credential_type]
+ }
+
+ )}
+ >
+ );
+}
+
+function CredentialForm({ credential = {}, onSubmit, onCancel, ...rest }) {
+ const initialValues = {
+ name: credential.name || undefined,
+ description: credential.description || undefined,
+ organization:
+ (credential.summary_fields && credential.summary_fields.organization) ||
+ null,
+ credential_type: credential.credential_type || undefined,
+ inputs: {
+ username: (credential.inputs && credential.inputs.username) || undefined,
+ password: (credential.inputs && credential.inputs.password) || undefined,
+ ssh_key_data:
+ (credential.inputs && credential.inputs.ssh_key_data) || undefined,
+ ssh_public_key_data:
+ (credential.inputs && credential.inputs.ssh_public_key_data) ||
+ undefined,
+ ssh_key_unlock:
+ (credential.inputs && credential.inputs.ssh_key_unlock) || undefined,
+ become_method:
+ (credential.inputs && credential.inputs.become_method) || undefined,
+ become_username:
+ (credential.inputs && credential.inputs.become_username) || undefined,
+ become_password:
+ (credential.inputs && credential.inputs.become_password) || undefined,
+ },
+ };
+
+ return (
+ {
+ onSubmit(values);
+ }}
+ >
+ {formik => (
+
+ )}
+
+ );
+}
+
+CredentialForm.proptype = {
+ handleSubmit: func.isRequired,
+ handleCancel: func.isRequired,
+ credential: shape({}),
+};
+
+CredentialForm.defaultProps = {
+ credential: {},
+};
+
+export default withI18n()(CredentialForm);
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx
new file mode 100644
index 0000000000..16fc3d3867
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx
@@ -0,0 +1,516 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+
+import CredentialForm from './CredentialForm';
+
+const machineCredential = {
+ id: 3,
+ type: 'credential',
+ url: '/api/v2/credentials/3/',
+ related: {
+ named_url: '/api/v2/credentials/oersdgfasf++Machine+ssh++org/',
+ created_by: '/api/v2/users/1/',
+ modified_by: '/api/v2/users/1/',
+ organization: '/api/v2/organizations/1/',
+ activity_stream: '/api/v2/credentials/3/activity_stream/',
+ access_list: '/api/v2/credentials/3/access_list/',
+ object_roles: '/api/v2/credentials/3/object_roles/',
+ owner_users: '/api/v2/credentials/3/owner_users/',
+ owner_teams: '/api/v2/credentials/3/owner_teams/',
+ copy: '/api/v2/credentials/3/copy/',
+ input_sources: '/api/v2/credentials/3/input_sources/',
+ credential_type: '/api/v2/credential_types/1/',
+ },
+ summary_fields: {
+ organization: {
+ id: 1,
+ name: 'org',
+ description: '',
+ },
+ credential_type: {
+ id: 1,
+ name: 'Machine',
+ description: '',
+ },
+ created_by: {
+ id: 1,
+ username: 'admin',
+ first_name: '',
+ last_name: '',
+ },
+ modified_by: {
+ id: 1,
+ username: 'admin',
+ first_name: '',
+ last_name: '',
+ },
+ object_roles: {
+ admin_role: {
+ description: 'Can manage all aspects of the credential',
+ name: 'Admin',
+ id: 36,
+ },
+ use_role: {
+ description: 'Can use the credential in a job template',
+ name: 'Use',
+ id: 37,
+ },
+ read_role: {
+ description: 'May view settings for the credential',
+ name: 'Read',
+ id: 38,
+ },
+ },
+ user_capabilities: {
+ edit: true,
+ delete: true,
+ copy: true,
+ use: true,
+ },
+ owners: [
+ {
+ id: 1,
+ type: 'user',
+ name: 'admin',
+ description: ' ',
+ url: '/api/v2/users/1/',
+ },
+ {
+ id: 1,
+ type: 'organization',
+ name: 'org',
+ description: '',
+ url: '/api/v2/organizations/1/',
+ },
+ ],
+ },
+ created: '2020-02-18T15:35:04.563928Z',
+ modified: '2020-02-18T15:35:04.563957Z',
+ name: 'oersdgfasf',
+ description: '',
+ organization: 1,
+ credential_type: 1,
+ inputs: {},
+ kind: 'ssh',
+ cloud: false,
+ kubernetes: false,
+};
+
+const sourceControlCredential = {
+ id: 4,
+ type: 'credential',
+ url: '/api/v2/credentials/4/',
+ related: {
+ named_url: '/api/v2/credentials/joijoij++Source Control+scm++/',
+ created_by: '/api/v2/users/1/',
+ modified_by: '/api/v2/users/1/',
+ activity_stream: '/api/v2/credentials/4/activity_stream/',
+ access_list: '/api/v2/credentials/4/access_list/',
+ object_roles: '/api/v2/credentials/4/object_roles/',
+ owner_users: '/api/v2/credentials/4/owner_users/',
+ owner_teams: '/api/v2/credentials/4/owner_teams/',
+ copy: '/api/v2/credentials/4/copy/',
+ input_sources: '/api/v2/credentials/4/input_sources/',
+ credential_type: '/api/v2/credential_types/2/',
+ user: '/api/v2/users/1/',
+ },
+ summary_fields: {
+ credential_type: {
+ id: 2,
+ name: 'Source Control',
+ description: '',
+ },
+ created_by: {
+ id: 1,
+ username: 'admin',
+ first_name: '',
+ last_name: '',
+ },
+ modified_by: {
+ id: 1,
+ username: 'admin',
+ first_name: '',
+ last_name: '',
+ },
+ object_roles: {
+ admin_role: {
+ description: 'Can manage all aspects of the credential',
+ name: 'Admin',
+ id: 39,
+ },
+ use_role: {
+ description: 'Can use the credential in a job template',
+ name: 'Use',
+ id: 40,
+ },
+ read_role: {
+ description: 'May view settings for the credential',
+ name: 'Read',
+ id: 41,
+ },
+ },
+ user_capabilities: {
+ edit: true,
+ delete: true,
+ copy: true,
+ use: true,
+ },
+ owners: [
+ {
+ id: 1,
+ type: 'user',
+ name: 'admin',
+ description: ' ',
+ url: '/api/v2/users/1/',
+ },
+ ],
+ },
+ created: '2020-02-18T16:03:01.366287Z',
+ modified: '2020-02-18T16:03:01.366315Z',
+ name: 'joijoij',
+ description: 'ojiojojo',
+ organization: null,
+ credential_type: 2,
+ inputs: {
+ ssh_key_unlock: '$encrypted$',
+ },
+ kind: 'scm',
+ cloud: false,
+ kubernetes: false,
+};
+
+const credentialTypes = [
+ {
+ id: 2,
+ type: 'credential_type',
+ url: '/api/v2/credential_types/2/',
+ related: {
+ credentials: '/api/v2/credential_types/2/credentials/',
+ activity_stream: '/api/v2/credential_types/2/activity_stream/',
+ },
+ summary_fields: {
+ user_capabilities: {
+ edit: false,
+ delete: false,
+ },
+ },
+ created: '2020-02-12T19:42:43.551238Z',
+ modified: '2020-02-12T19:43:03.164800Z',
+ name: 'Source Control',
+ description: '',
+ kind: 'scm',
+ namespace: 'scm',
+ managed_by_tower: true,
+ inputs: {
+ fields: [
+ {
+ id: 'username',
+ label: 'Username',
+ type: 'string',
+ },
+ {
+ id: 'password',
+ label: 'Password',
+ type: 'string',
+ secret: true,
+ },
+ {
+ id: 'ssh_key_data',
+ label: 'SCM Private Key',
+ type: 'string',
+ format: 'ssh_private_key',
+ secret: true,
+ multiline: true,
+ },
+ {
+ id: 'ssh_key_unlock',
+ label: 'Private Key Passphrase',
+ type: 'string',
+ secret: true,
+ },
+ ],
+ },
+ injectors: {},
+ },
+ {
+ id: 1,
+ type: 'credential_type',
+ url: '/api/v2/credential_types/1/',
+ related: {
+ credentials: '/api/v2/credential_types/1/credentials/',
+ activity_stream: '/api/v2/credential_types/1/activity_stream/',
+ },
+ summary_fields: {
+ user_capabilities: {
+ edit: false,
+ delete: false,
+ },
+ },
+ created: '2020-02-12T19:42:43.539626Z',
+ modified: '2020-02-12T19:43:03.159739Z',
+ name: 'Machine',
+ description: '',
+ kind: 'ssh',
+ namespace: 'ssh',
+ managed_by_tower: true,
+ inputs: {
+ fields: [
+ {
+ id: 'username',
+ label: 'Username',
+ type: 'string',
+ },
+ {
+ id: 'password',
+ label: 'Password',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ {
+ id: 'ssh_key_data',
+ label: 'SSH Private Key',
+ type: 'string',
+ format: 'ssh_private_key',
+ secret: true,
+ multiline: true,
+ },
+ {
+ id: 'ssh_public_key_data',
+ label: 'Signed SSH Certificate',
+ type: 'string',
+ multiline: true,
+ secret: true,
+ },
+ {
+ id: 'ssh_key_unlock',
+ label: 'Private Key Passphrase',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ {
+ id: 'become_method',
+ label: 'Privilege Escalation Method',
+ type: 'string',
+ help_text:
+ 'Specify a method for "become" operations. This is equivalent to specifying the --become-method Ansible parameter.',
+ },
+ {
+ id: 'become_username',
+ label: 'Privilege Escalation Username',
+ type: 'string',
+ },
+ {
+ id: 'become_password',
+ label: 'Privilege Escalation Password',
+ type: 'string',
+ secret: true,
+ ask_at_runtime: true,
+ },
+ ],
+ },
+ injectors: {},
+ },
+];
+
+describe('', () => {
+ let wrapper;
+ let onCancel;
+ let onSubmit;
+
+ const addFieldExpects = () => {
+ expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Username"]').length).toBe(0);
+ expect(wrapper.find('FormGroup[label="Password"]').length).toBe(0);
+ expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Signed SSH Certificate"]').length
+ ).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Private Key Passphrase"]').length
+ ).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Privelege Escalation Method"]').length
+ ).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Privilege Escalation Username"]').length
+ ).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Privilege Escalation Password"]').length
+ ).toBe(0);
+ };
+
+ const machineFieldExpects = () => {
+ expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="Signed SSH Certificate"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="Private Key Passphrase"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="Privelege Escalation Method"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="Privilege Escalation Username"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="Privilege Escalation Password"]').length
+ ).toBe(1);
+ };
+
+ const sourceFieldExpects = () => {
+ expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="Signed SSH Certificate"]').length
+ ).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Private Key Passphrase"]').length
+ ).toBe(1);
+ expect(
+ wrapper.find('FormGroup[label="Privelege Escalation Method"]').length
+ ).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Privilege Escalation Username"]').length
+ ).toBe(0);
+ expect(
+ wrapper.find('FormGroup[label="Privilege Escalation Password"]').length
+ ).toBe(0);
+ };
+
+ beforeEach(() => {
+ onCancel = jest.fn();
+ onSubmit = jest.fn();
+ wrapper = mountWithContexts(
+
+ );
+ });
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('Initially renders successfully', () => {
+ expect(wrapper.length).toBe(1);
+ });
+
+ test('should display form fields on add properly', () => {
+ wrapper = mountWithContexts(
+
+ );
+ addFieldExpects();
+ });
+
+ test('should display form fields for machine credential properly', () => {
+ wrapper = mountWithContexts(
+
+ );
+ machineFieldExpects();
+ });
+
+ test('should display form fields for source control credential properly', () => {
+ wrapper = mountWithContexts(
+
+ );
+ sourceFieldExpects();
+ });
+
+ test('should update form values', async () => {
+ // name and description change
+ act(() => {
+ wrapper.find('input#credential-name').simulate('change', {
+ target: { value: 'new Foo', name: 'name' },
+ });
+ wrapper.find('input#credential-description').simulate('change', {
+ target: { value: 'new Bar', name: 'description' },
+ });
+ });
+ wrapper.update();
+ expect(wrapper.find('input#credential-name').prop('value')).toEqual(
+ 'new Foo'
+ );
+ expect(wrapper.find('input#credential-description').prop('value')).toEqual(
+ 'new Bar'
+ );
+ // organization change
+ act(() => {
+ wrapper.find('OrganizationLookup').invoke('onBlur')();
+ wrapper.find('OrganizationLookup').invoke('onChange')({
+ id: 3,
+ name: 'organization',
+ });
+ });
+ wrapper.update();
+ expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({
+ id: 3,
+ name: 'organization',
+ });
+ });
+
+ test('should display cred type subform when scm type select has a value', async () => {
+ wrapper = mountWithContexts(
+
+ );
+ addFieldExpects();
+ await act(async () => {
+ await wrapper
+ .find('AnsibleSelect[id="credential_type"]')
+ .invoke('onChange')(null, 1);
+ });
+ wrapper.update();
+ machineFieldExpects();
+ await act(async () => {
+ await wrapper
+ .find('AnsibleSelect[id="credential_type"]')
+ .invoke('onChange')(null, 2);
+ });
+ wrapper.update();
+ sourceFieldExpects();
+ });
+
+ test('should call handleCancel when Cancel button is clicked', async () => {
+ expect(onCancel).not.toHaveBeenCalled();
+ wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
+ expect(onCancel).toBeCalled();
+ });
+});
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/ManualSubForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/ManualSubForm.jsx
new file mode 100644
index 0000000000..29f37547af
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/ManualSubForm.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { useField } from 'formik';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import FormField, { PasswordField } from '@components/FormField';
+import { FormGroup } from '@patternfly/react-core';
+import AnsibleSelect from '@components/AnsibleSelect';
+import { FormColumnLayout, FormFullWidthLayout } from '@components/FormLayout';
+import {
+ UsernameFormField,
+ PasswordFormField,
+ SSHKeyUnlockField,
+ SSHKeyDataField,
+} from './SharedFields';
+
+const ManualSubForm = ({ i18n }) => {
+ const becomeMethodOptions = [
+ {
+ value: '',
+ key: '',
+ label: i18n._(t`Choose a Privelege Escalation Method`),
+ isDisabled: true,
+ },
+ ...[
+ 'sudo',
+ 'su',
+ 'pbrun',
+ 'pfexec',
+ 'dzdo',
+ 'pmrun',
+ 'runas',
+ 'enable',
+ 'doas',
+ 'ksu',
+ 'machinectl',
+ 'sesu',
+ ].map(val => ({ value: val, key: val, label: val })),
+ ];
+
+ const becomeMethodFieldArr = useField('inputs.become_method');
+ const becomeMethodField = becomeMethodFieldArr[0];
+ const becomeMethodHelpers = becomeMethodFieldArr[2];
+
+ return (
+
+
+
+
+
+
+
+
+
+ {
+ becomeMethodHelpers.setValue(value);
+ }}
+ />
+
+
+
+
+ );
+};
+
+export default withI18n()(ManualSubForm);
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx
new file mode 100644
index 0000000000..630adfc1ef
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import FormField, { PasswordField } from '@components/FormField';
+import { Title } from '@patternfly/react-core';
+import styled from 'styled-components';
+
+export const UsernameFormField = withI18n()(({ i18n }) => (
+
+));
+
+export const PasswordFormField = withI18n()(({ i18n }) => (
+
+));
+
+export const SSHKeyDataField = withI18n()(({ i18n }) => (
+
+));
+
+export const SSHKeyUnlockField = withI18n()(({ i18n }) => (
+
+));
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx
new file mode 100644
index 0000000000..5a46465e65
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { withI18n } from '@lingui/react';
+import { FormColumnLayout, FormFullWidthLayout } from '@components/FormLayout';
+import {
+ UsernameFormField,
+ PasswordFormField,
+ SSHKeyUnlockField,
+ SSHKeyDataField,
+} from './SharedFields';
+
+const SourceControlSubForm = () => (
+ <>
+
+
+
+
+
+
+
+
+ >
+);
+
+export default withI18n()(SourceControlSubForm);
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js
new file mode 100644
index 0000000000..04cad7240e
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js
@@ -0,0 +1,2 @@
+export { default as ManualSubForm } from './ManualSubForm';
+export { default as SourceControlSubForm } from './SourceControlSubForm';
diff --git a/awx/ui_next/src/screens/Credential/shared/data.credential.json b/awx/ui_next/src/screens/Credential/shared/data.credential.json
new file mode 100644
index 0000000000..c57ee690e5
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/data.credential.json
@@ -0,0 +1,84 @@
+{
+ "id": 2,
+ "type": "credential",
+ "url": "/api/v2/credentials/2/",
+ "related": {
+ "named_url": "/api/v2/credentials/jojoijoij++Source Control+scm++/",
+ "created_by": "/api/v2/users/1/",
+ "modified_by": "/api/v2/users/1/",
+ "activity_stream": "/api/v2/credentials/2/activity_stream/",
+ "access_list": "/api/v2/credentials/2/access_list/",
+ "object_roles": "/api/v2/credentials/2/object_roles/",
+ "owner_users": "/api/v2/credentials/2/owner_users/",
+ "owner_teams": "/api/v2/credentials/2/owner_teams/",
+ "copy": "/api/v2/credentials/2/copy/",
+ "input_sources": "/api/v2/credentials/2/input_sources/",
+ "credential_type": "/api/v2/credential_types/2/",
+ "user": "/api/v2/users/1/"
+ },
+ "summary_fields": {
+ "credential_type": {
+ "id": 2,
+ "name": "Source Control",
+ "description": ""
+ },
+ "created_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "object_roles": {
+ "admin_role": {
+ "description": "Can manage all aspects of the credential",
+ "name": "Admin",
+ "id": 6
+ },
+ "use_role": {
+ "description": "Can use the credential in a job template",
+ "name": "Use",
+ "id": 7
+ },
+ "read_role": {
+ "description": "May view settings for the credential",
+ "name": "Read",
+ "id": 8
+ }
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true,
+ "use": true
+ },
+ "owners": [
+ {
+ "id": 1,
+ "type": "user",
+ "name": "admin",
+ "description": " ",
+ "url": "/api/v2/users/1/"
+ }
+ ]
+ },
+ "created": "2020-02-12T19:59:11.508933Z",
+ "modified": "2020-02-12T19:59:11.508958Z",
+ "name": "jojoijoij",
+ "description": "",
+ "organization": null,
+ "credential_type": 2,
+ "inputs": {
+ "password": "$encrypted$",
+ "username": "uujoij",
+ "ssh_key_unlock": "$encrypted$"
+ },
+ "kind": "scm",
+ "cloud": false,
+ "kubernetes": false
+}
diff --git a/awx/ui_next/src/screens/Credential/shared/data.orgCredential.json b/awx/ui_next/src/screens/Credential/shared/data.orgCredential.json
new file mode 100644
index 0000000000..621c3a43ad
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/data.orgCredential.json
@@ -0,0 +1,92 @@
+{
+ "id": 3,
+ "type": "credential",
+ "url": "/api/v2/credentials/3/",
+ "related": {
+ "named_url": "/api/v2/credentials/oersdgfasf++Machine+ssh++org/",
+ "created_by": "/api/v2/users/1/",
+ "modified_by": "/api/v2/users/1/",
+ "organization": "/api/v2/organizations/1/",
+ "activity_stream": "/api/v2/credentials/3/activity_stream/",
+ "access_list": "/api/v2/credentials/3/access_list/",
+ "object_roles": "/api/v2/credentials/3/object_roles/",
+ "owner_users": "/api/v2/credentials/3/owner_users/",
+ "owner_teams": "/api/v2/credentials/3/owner_teams/",
+ "copy": "/api/v2/credentials/3/copy/",
+ "input_sources": "/api/v2/credentials/3/input_sources/",
+ "credential_type": "/api/v2/credential_types/1/"
+ },
+ "summary_fields": {
+ "organization": {
+ "id": 1,
+ "name": "org",
+ "description": ""
+ },
+ "credential_type": {
+ "id": 1,
+ "name": "Machine",
+ "description": ""
+ },
+ "created_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "object_roles": {
+ "admin_role": {
+ "description": "Can manage all aspects of the credential",
+ "name": "Admin",
+ "id": 36
+ },
+ "use_role": {
+ "description": "Can use the credential in a job template",
+ "name": "Use",
+ "id": 37
+ },
+ "read_role": {
+ "description": "May view settings for the credential",
+ "name": "Read",
+ "id": 38
+ }
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true,
+ "use": true
+ },
+ "owners": [
+ {
+ "id": 1,
+ "type": "user",
+ "name": "admin",
+ "description": " ",
+ "url": "/api/v2/users/1/"
+ },
+ {
+ "id": 1,
+ "type": "organization",
+ "name": "org",
+ "description": "",
+ "url": "/api/v2/organizations/1/"
+ }
+ ]
+ },
+ "created": "2020-02-18T15:35:04.563928Z",
+ "modified": "2020-02-18T15:35:04.563957Z",
+ "name": "oersdgfasf",
+ "description": "",
+ "organization": 1,
+ "credential_type": 1,
+ "inputs": {},
+ "kind": "ssh",
+ "cloud": false,
+ "kubernetes": false
+}