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}