diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
new file mode 100644
index 0000000000..964b301d8a
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Card, CardBody, PageSection } from '@patternfly/react-core';
+
+function CredentialAdd() {
+ return (
+
+
+ Coming soon :)
+
+
+ );
+}
+
+export default CredentialAdd;
diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/index.js b/awx/ui_next/src/screens/Credential/CredentialAdd/index.js
new file mode 100644
index 0000000000..46ece18e1d
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialAdd/index.js
@@ -0,0 +1 @@
+export { default } from './CredentialAdd';
diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx
new file mode 100644
index 0000000000..ee94b492c8
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx
@@ -0,0 +1,169 @@
+import React, { useState, useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { CredentialsAPI } from '@api';
+import { Card, PageSection } from '@patternfly/react-core';
+import AlertModal from '@components/AlertModal';
+import ErrorDetail from '@components/ErrorDetail';
+import DataListToolbar from '@components/DataListToolbar';
+import PaginatedDataList, {
+ ToolbarAddButton,
+ ToolbarDeleteButton,
+} from '@components/PaginatedDataList';
+import { getQSConfig, parseQueryString } from '@util/qs';
+import { CredentialListItem } from '.';
+
+const QS_CONFIG = getQSConfig('credential', {
+ page: 1,
+ page_size: 20,
+ order_by: 'name',
+});
+
+function CredentialList({ i18n }) {
+ const [actions, setActions] = useState(null);
+ const [contentError, setContentError] = useState(null);
+ const [credentialCount, setCredentialCount] = useState(0);
+ const [credentials, setCredentials] = useState([]);
+ const [deletionError, setDeletionError] = useState(null);
+ const [hasContentLoading, setHasContentLoading] = useState(true);
+ const [selected, setSelected] = useState([]);
+
+ const location = useLocation();
+
+ const loadCredentials = async ({ search }) => {
+ const params = parseQueryString(QS_CONFIG, search);
+ setContentError(null);
+ setHasContentLoading(true);
+ try {
+ const [
+ {
+ data: { count, results },
+ },
+ {
+ data: { actions: optionActions },
+ },
+ ] = await Promise.all([
+ CredentialsAPI.read(params),
+ loadCredentialActions(),
+ ]);
+
+ setCredentials(results);
+ setCredentialCount(count);
+ setActions(optionActions);
+ } catch (error) {
+ setContentError(error);
+ } finally {
+ setHasContentLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadCredentials(location);
+ }, [location]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const loadCredentialActions = () => {
+ if (actions) {
+ return Promise.resolve({ data: { actions } });
+ }
+ return CredentialsAPI.readOptions();
+ };
+
+ const handleSelectAll = isSelected => {
+ setSelected(isSelected ? [...credentials] : []);
+ };
+
+ const handleSelect = row => {
+ if (selected.some(s => s.id === row.id)) {
+ setSelected(selected.filter(s => s.id !== row.id));
+ } else {
+ setSelected(selected.concat(row));
+ }
+ };
+
+ const handleDelete = async () => {
+ setHasContentLoading(true);
+
+ try {
+ await Promise.all(
+ selected.map(credential => CredentialsAPI.destroy(credential.id))
+ );
+ } catch (error) {
+ setDeletionError(error);
+ }
+
+ const params = parseQueryString(QS_CONFIG, location.search);
+ try {
+ const {
+ data: { count, results },
+ } = await CredentialsAPI.read(params);
+
+ setCredentials(results);
+ setCredentialCount(count);
+ setSelected([]);
+ } catch (error) {
+ setContentError(error);
+ }
+
+ setHasContentLoading(false);
+ };
+
+ const canAdd =
+ actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
+ const isAllSelected =
+ selected.length > 0 && selected.length === credentials.length;
+
+ return (
+
+
+ (
+ row.id === item.id)}
+ onSelect={() => handleSelect(item)}
+ />
+ )}
+ renderToolbar={props => (
+ ,
+ canAdd && (
+
+ ),
+ ]}
+ />
+ )}
+ />
+
+ setDeletionError(null)}
+ >
+ {i18n._(t`Failed to delete one or more credentials.`)}
+
+
+
+ );
+}
+
+export default withI18n()(CredentialList);
diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx
new file mode 100644
index 0000000000..fc45442a5d
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx
@@ -0,0 +1,139 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { CredentialsAPI } from '@api';
+import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { CredentialList } from '.';
+import mockCredentials from '../shared';
+
+jest.mock('@api');
+
+describe('', () => {
+ let wrapper;
+
+ beforeEach(async () => {
+ CredentialsAPI.read.mockResolvedValueOnce({ data: mockCredentials });
+ CredentialsAPI.readOptions.mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
+ },
+ },
+ });
+
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ wrapper.unmount();
+ });
+
+ test('initially renders successfully', () => {
+ expect(wrapper.find('CredentialList').length).toBe(1);
+ });
+
+ test('should fetch credentials from api and render the in the list', () => {
+ expect(CredentialsAPI.read).toHaveBeenCalled();
+ expect(wrapper.find('CredentialListItem').length).toBe(5);
+ });
+
+ test('should show content error if credentials are not successfully fetched from api', async () => {
+ CredentialsAPI.readOptions.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ await waitForElement(wrapper, 'ContentError', el => el.length === 1);
+ });
+
+ test('should check and uncheck the row item', async () => {
+ expect(
+ wrapper.find('PFDataListCheck[id="select-credential-1"]').props().checked
+ ).toBe(false);
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-credential-1"]')
+ .invoke('onChange')(true);
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('PFDataListCheck[id="select-credential-1"]').props().checked
+ ).toBe(true);
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-credential-1"]')
+ .invoke('onChange')(false);
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('PFDataListCheck[id="select-credential-1"]').props().checked
+ ).toBe(false);
+ });
+
+ test('should check all row items when select all is checked', async () => {
+ wrapper.find('PFDataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(false);
+ });
+ await act(async () => {
+ wrapper.find('Checkbox#select-all').invoke('onChange')(true);
+ });
+ wrapper.update();
+ wrapper.find('PFDataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(true);
+ });
+ await act(async () => {
+ wrapper.find('Checkbox#select-all').invoke('onChange')(false);
+ });
+ wrapper.update();
+ wrapper.find('PFDataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(false);
+ });
+ });
+
+ test('should call api delete credentials for each selected credential', async () => {
+ CredentialsAPI.read.mockResolvedValueOnce({ data: mockCredentials });
+ CredentialsAPI.destroy = jest.fn();
+
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-credential-3"]')
+ .invoke('onChange')();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
+ });
+ wrapper.update();
+ expect(CredentialsAPI.destroy).toHaveBeenCalledTimes(1);
+ });
+
+ test('should show error modal when credential is not successfully deleted from api', async () => {
+ CredentialsAPI.destroy.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-credential-2"]')
+ .invoke('onChange')();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
+ });
+ await waitForElement(
+ wrapper,
+ 'Modal',
+ el => el.props().isOpen === true && el.props().title === 'Error!'
+ );
+ await act(async () => {
+ wrapper.find('ModalBoxCloseButton').invoke('onClose')();
+ });
+ await waitForElement(wrapper, 'Modal', el => el.props().isOpen === false);
+ });
+});
diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx
new file mode 100644
index 0000000000..ccb58269b0
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { string, bool, func } from 'prop-types';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Link } from 'react-router-dom';
+import {
+ DataListItem,
+ DataListItemRow,
+ DataListItemCells as _DataListItemCells,
+ Tooltip,
+} from '@patternfly/react-core';
+import { PencilAltIcon } from '@patternfly/react-icons';
+
+import ActionButtonCell from '@components/ActionButtonCell';
+import DataListCell from '@components/DataListCell';
+import DataListCheck from '@components/DataListCheck';
+import ListActionButton from '@components/ListActionButton';
+import VerticalSeparator from '@components/VerticalSeparator';
+import styled from 'styled-components';
+import { Credential } from '@types';
+
+const DataListItemCells = styled(_DataListItemCells)`
+ ${DataListCell}:first-child {
+ flex-grow: 2;
+ }
+`;
+
+function CredentialListItem({
+ credential,
+ detailUrl,
+ isSelected,
+ onSelect,
+ i18n,
+}) {
+ const labelId = `check-action-${credential.id}`;
+ const canEdit = credential.summary_fields.user_capabilities.edit;
+
+ return (
+
+
+
+
+
+
+ {credential.name}
+
+ ,
+
+ {credential.summary_fields.credential_type.name}
+ ,
+
+ {canEdit && (
+
+
+
+
+
+ )}
+ ,
+ ]}
+ />
+
+
+ );
+}
+
+CredentialListItem.propTypes = {
+ detailUrl: string.isRequired,
+ credential: Credential.isRequired,
+ isSelected: bool.isRequired,
+ onSelect: func.isRequired,
+};
+
+export default withI18n()(CredentialListItem);
diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx
new file mode 100644
index 0000000000..bb6e62dc6c
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { CredentialListItem } from '.';
+import mockCredentials from '../shared';
+
+describe('', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('edit button shown to users with edit capabilities', () => {
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
+ });
+
+ test('edit button hidden from users without edit capabilities', () => {
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
+ });
+});
diff --git a/awx/ui_next/src/screens/Credential/CredentialList/index.js b/awx/ui_next/src/screens/Credential/CredentialList/index.js
new file mode 100644
index 0000000000..0e8ca914a3
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/CredentialList/index.js
@@ -0,0 +1,2 @@
+export { default as CredentialList } from './CredentialList';
+export { default as CredentialListItem } from './CredentialListItem';
diff --git a/awx/ui_next/src/screens/Credential/Credentials.jsx b/awx/ui_next/src/screens/Credential/Credentials.jsx
index 8f737fe765..3c2e196621 100644
--- a/awx/ui_next/src/screens/Credential/Credentials.jsx
+++ b/awx/ui_next/src/screens/Credential/Credentials.jsx
@@ -1,26 +1,27 @@
-import React, { Component, Fragment } from 'react';
+import React from 'react';
+import { Route, Switch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-import {
- PageSection,
- PageSectionVariants,
- Title,
-} from '@patternfly/react-core';
-class Credentials extends Component {
- render() {
- const { i18n } = this.props;
- const { light } = PageSectionVariants;
+import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
+import { CredentialList } from './CredentialList';
+import CredentialAdd from './CredentialAdd';
- return (
-
-
- {i18n._(t`Credentials`)}
-
-
-
- );
- }
+function Credentials({ i18n }) {
+ const breadcrumbConfig = {
+ '/credentials': i18n._(t`Credentials`),
+ '/credentials/add': i18n._(t`Create New Credential`),
+ };
+
+ return (
+ <>
+
+
+ } />
+ } />
+
+ >
+ );
}
export default withI18n()(Credentials);
diff --git a/awx/ui_next/src/screens/Credential/Credentials.test.jsx b/awx/ui_next/src/screens/Credential/Credentials.test.jsx
index 1e835ba932..b87815c9d8 100644
--- a/awx/ui_next/src/screens/Credential/Credentials.test.jsx
+++ b/awx/ui_next/src/screens/Credential/Credentials.test.jsx
@@ -1,29 +1,58 @@
import React from 'react';
-
import { mountWithContexts } from '@testUtils/enzymeHelpers';
-
+import { createMemoryHistory } from 'history';
import Credentials from './Credentials';
describe('', () => {
- let pageWrapper;
- let pageSections;
- let title;
-
- beforeEach(() => {
- pageWrapper = mountWithContexts();
- pageSections = pageWrapper.find('PageSection');
- title = pageWrapper.find('Title');
- });
+ let wrapper;
afterEach(() => {
- pageWrapper.unmount();
+ wrapper.unmount();
});
- test('initially renders without crashing', () => {
- expect(pageWrapper.length).toBe(1);
- expect(pageSections.length).toBe(2);
- expect(title.length).toBe(1);
- expect(title.props().size).toBe('2xl');
- expect(pageSections.first().props().variant).toBe('light');
+ test('initially renders succesfully', () => {
+ wrapper = mountWithContexts();
+ });
+
+ test('should display credential list breadcrumb heading', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/credentials'],
+ });
+
+ wrapper = mountWithContexts(, {
+ context: {
+ router: {
+ history,
+ route: {
+ location: history.location,
+ },
+ },
+ },
+ });
+
+ expect(wrapper.find('Crumb').length).toBe(1);
+ expect(wrapper.find('BreadcrumbHeading').text()).toBe('Credentials');
+ });
+
+ test('should display create new credential breadcrumb heading', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/credentials/add'],
+ });
+
+ wrapper = mountWithContexts(, {
+ context: {
+ router: {
+ history,
+ route: {
+ location: history.location,
+ },
+ },
+ },
+ });
+
+ expect(wrapper.find('Crumb').length).toBe(2);
+ expect(wrapper.find('BreadcrumbHeading').text()).toBe(
+ 'Create New Credential'
+ );
});
});
diff --git a/awx/ui_next/src/screens/Credential/shared/data.credentials.json b/awx/ui_next/src/screens/Credential/shared/data.credentials.json
new file mode 100644
index 0000000000..60d4b71d05
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/data.credentials.json
@@ -0,0 +1,391 @@
+{
+ "count": 5,
+ "next": "/api/v2/credentials/",
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "type": "credential",
+ "url": "/api/v2/credentials/1/",
+ "related": {
+ "created_by": "/api/v2/users/1/",
+ "modified_by": "/api/v2/users/1/",
+ "activity_stream": "/api/v2/credentials/1/activity_stream/",
+ "access_list": "/api/v2/credentials/1/access_list/",
+ "object_roles": "/api/v2/credentials/1/object_roles/",
+ "owner_users": "/api/v2/credentials/1/owner_users/",
+ "owner_teams": "/api/v2/credentials/1/owner_teams/",
+ "copy": "/api/v2/credentials/1/copy/",
+ "input_sources": "/api/v2/credentials/1/input_sources/",
+ "credential_type": "/api/v2/credential_types/23/",
+ "user": "/api/v2/users/7/"
+ },
+ "summary_fields": {
+ "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": 284
+ },
+ "use_role": {
+ "description": "Can use the credential in a job template",
+ "name": "Use",
+ "id": 285
+ },
+ "read_role": {
+ "description": "May view settings for the credential",
+ "name": "Read",
+ "id": 286
+ }
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true,
+ "use": true
+ }
+ },
+ "created": "2019-12-17T16:12:25.258897Z",
+ "modified": "2019-12-17T16:12:25.258920Z",
+ "name": "Foo",
+ "organization": null,
+ "credential_type": 1,
+ "kind": null,
+ "cloud": true,
+ "kubernetes": false
+ },
+ {
+ "id": 2,
+ "type": "credential",
+ "url": "/api/v2/credentials/2/",
+ "related": {
+ "created_by": "/api/v2/users/8/",
+ "modified_by": "/api/v2/users/12/",
+ "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/1/",
+ "user": "/api/v2/users/7/"
+ },
+ "summary_fields": {
+ "credential_type": {
+ "id": 1,
+ "name": "Machine",
+ "description": ""
+ },
+ "created_by": {
+ "id": 8,
+ "username": "user-2",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 12,
+ "username": "user-6",
+ "first_name": "",
+ "last_name": ""
+ },
+ "object_roles": {
+ "admin_role": {
+ "description": "Can manage all aspects of the credential",
+ "name": "Admin",
+ "id": 81
+ },
+ "use_role": {
+ "description": "Can use the credential in a job template",
+ "name": "Use",
+ "id": 82
+ },
+ "read_role": {
+ "description": "May view settings for the credential",
+ "name": "Read",
+ "id": 83
+ }
+ },
+ "user_capabilities": {
+ "edit": false,
+ "delete": false,
+ "copy": false,
+ "use": false
+ },
+ "owners": [
+ {
+ "id": 7,
+ "type": "user",
+ "name": "user-1",
+ "description": " ",
+ "url": "/api/v2/users/7/"
+ }
+ ]
+ },
+ "created": "2019-12-16T21:04:40.896097Z",
+ "modified": "2019-12-16T21:04:40.896121Z",
+ "name": "Bar",
+ "description": "",
+ "organization": null,
+ "credential_type": 1,
+ "inputs": {},
+ "kind": "ssh",
+ "cloud": false,
+ "kubernetes": false
+ },
+ {
+ "id": 3,
+ "type": "credential",
+ "url": "/api/v2/credentials/3/",
+ "related": {
+ "created_by": "/api/v2/users/9/",
+ "modified_by": "/api/v2/users/13/",
+ "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/",
+ "user": "/api/v2/users/8/"
+ },
+ "summary_fields": {
+ "credential_type": {
+ "id": 1,
+ "name": "Machine",
+ "description": ""
+ },
+ "created_by": {
+ "id": 9,
+ "username": "user-3",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 13,
+ "username": "user-7",
+ "first_name": "",
+ "last_name": ""
+ },
+ "object_roles": {
+ "admin_role": {
+ "description": "Can manage all aspects of the credential",
+ "name": "Admin",
+ "id": 84
+ },
+ "use_role": {
+ "description": "Can use the credential in a job template",
+ "name": "Use",
+ "id": 85
+ },
+ "read_role": {
+ "description": "May view settings for the credential",
+ "name": "Read",
+ "id": 86
+ }
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true,
+ "use": true
+ },
+ "owners": [
+ {
+ "id": 8,
+ "type": "user",
+ "name": "user-2",
+ "description": " ",
+ "url": "/api/v2/users/8/"
+ }
+ ]
+ },
+ "created": "2019-12-16T21:04:40.955954Z",
+ "modified": "2019-12-16T21:04:40.955976Z",
+ "name": "Baz",
+ "description": "",
+ "organization": null,
+ "credential_type": 1,
+ "inputs": {},
+ "kind": "ssh",
+ "cloud": false,
+ "kubernetes": false
+ },
+ {
+ "id": 4,
+ "type": "credential",
+ "url": "/api/v2/credentials/4/",
+ "related": {
+ "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/25/",
+ "user": "/api/v2/users/1/"
+ },
+ "summary_fields": {
+ "credential_type": {
+ "id": 2,
+ "name": "Vault",
+ "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": 318
+ },
+ "use_role": {
+ "description": "Can use the credential in a job template",
+ "name": "Use",
+ "id": 319
+ },
+ "read_role": {
+ "description": "May view settings for the credential",
+ "name": "Read",
+ "id": 320
+ }
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true,
+ "use": true
+ },
+ "owners": [
+ {
+ "id": 1,
+ "type": "user",
+ "name": "admin",
+ "description": " ",
+ "url": "/api/v2/users/1/"
+ }
+ ]
+ },
+ "created": "2019-12-18T16:31:09.772005Z",
+ "modified": "2019-12-18T16:31:09.832666Z",
+ "name": "FooBar",
+ "description": "",
+ "organization": null,
+ "credential_type": 2,
+ "inputs": {},
+ "kind": null,
+ "cloud": true,
+ "kubernetes": false
+ },
+ {
+ "id": 5,
+ "type": "credential",
+ "url": "/api/v2/credentials/5/",
+ "related": {
+ "created_by": "/api/v2/users/1/",
+ "modified_by": "/api/v2/users/1/",
+ "activity_stream": "/api/v2/credentials/5/activity_stream/",
+ "access_list": "/api/v2/credentials/5/access_list/",
+ "object_roles": "/api/v2/credentials/5/object_roles/",
+ "owner_users": "/api/v2/credentials/5/owner_users/",
+ "owner_teams": "/api/v2/credentials/5/owner_teams/",
+ "copy": "/api/v2/credentials/5/copy/",
+ "input_sources": "/api/v2/credentials/5/input_sources/",
+ "credential_type": "/api/v2/credential_types/25/",
+ "user": "/api/v2/users/1/"
+ },
+ "summary_fields": {
+ "credential_type": {
+ "id": 3,
+ "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": 290
+ },
+ "use_role": {
+ "description": "Can use the credential in a job template",
+ "name": "Use",
+ "id": 291
+ },
+ "read_role": {
+ "description": "May view settings for the credential",
+ "name": "Read",
+ "id": 292
+ }
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true,
+ "use": true
+ },
+ "owners": [
+ {
+ "id": 1,
+ "type": "user",
+ "name": "admin",
+ "description": " ",
+ "url": "/api/v2/users/1/"
+ }
+ ]
+ },
+ "created": "2019-12-17T16:12:44.923123Z",
+ "modified": "2019-12-17T16:12:44.923151Z",
+ "name": "Qux",
+ "description": "",
+ "organization": null,
+ "credential_type": 3,
+ "inputs": {},
+ "kind": null,
+ "cloud": true,
+ "kubernetes": false
+ }
+ ]
+}
\ No newline at end of file
diff --git a/awx/ui_next/src/screens/Credential/shared/index.js b/awx/ui_next/src/screens/Credential/shared/index.js
new file mode 100644
index 0000000000..723f2ff91e
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/index.js
@@ -0,0 +1 @@
+export { default } from './data.credentials.json';
diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx
index acb2eb537c..c2aaebcb0b 100644
--- a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx
+++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx
@@ -50,7 +50,7 @@ class HostListItem extends React.Component {
/>
+
{host.name}