From c4ff27cedbf072168e892d343f29dd016655591b Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Wed, 18 Dec 2019 17:03:52 -0500 Subject: [PATCH] Add Credential List and unit tests --- .../CredentialAdd/CredentialAdd.jsx | 14 + .../screens/Credential/CredentialAdd/index.js | 1 + .../CredentialList/CredentialList.jsx | 194 ++++++++++ .../CredentialList/CredentialList.test.jsx | 145 +++++++ .../CredentialList/CredentialListItem.jsx | 83 ++++ .../CredentialListItem.test.jsx | 36 ++ .../Credential/CredentialList/index.js | 2 + .../src/screens/Credential/Credentials.jsx | 39 +- .../screens/Credential/Credentials.test.jsx | 65 +++- .../shared/data.credential_types.json | 176 +++++++++ .../Credential/shared/data.credentials.json | 366 ++++++++++++++++++ .../src/screens/Credential/shared/index.js | 2 + .../screens/Host/HostList/HostListItem.jsx | 2 +- 13 files changed, 1087 insertions(+), 38 deletions(-) create mode 100644 awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx create mode 100644 awx/ui_next/src/screens/Credential/CredentialAdd/index.js create mode 100644 awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx create mode 100644 awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx create mode 100644 awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx create mode 100644 awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx create mode 100644 awx/ui_next/src/screens/Credential/CredentialList/index.js create mode 100644 awx/ui_next/src/screens/Credential/shared/data.credential_types.json create mode 100644 awx/ui_next/src/screens/Credential/shared/data.credentials.json create mode 100644 awx/ui_next/src/screens/Credential/shared/index.js 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..9881f418dd --- /dev/null +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx @@ -0,0 +1,194 @@ +import React, { useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { CredentialsAPI, CredentialTypesAPI } 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('project', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +const fetchCredentialTypes = async credentials => { + const typeIds = Array.from( + credentials.reduce((accumulator, credential) => { + accumulator.add(credential.credential_type); + return accumulator; + }, new Set()) + ); + + const { + data: { results }, + } = await CredentialTypesAPI.read({ + or__id: typeIds, + }); + + return results; +}; + +const assignCredentialKinds = (credentials, credentialTypes) => { + const typesById = credentialTypes.reduce((accumulator, type) => { + accumulator[type.id] = type.name; + return accumulator; + }, {}); + + credentials.forEach(credential => { + credential.kind = typesById[credential.credential_type]; + }); + + return credentials; +}; + +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 [isLoading, setIsLoading] = useState(true); + const [selected, setSelected] = useState([]); + + const location = useLocation(); + + useEffect(() => { + async function fetchData() { + const params = parseQueryString(QS_CONFIG, location.search); + + try { + const [ + { + data: { count, results }, + }, + { + data: { actions: optionActions }, + }, + ] = await Promise.all([ + CredentialsAPI.read(params), + CredentialsAPI.readOptions(), + ]); + + const credentialTypes = await fetchCredentialTypes(results); + + setCredentials(assignCredentialKinds(results, credentialTypes)); + setCredentialCount(count); + setActions(optionActions); + } catch (error) { + setContentError(error); + } finally { + setIsLoading(false); + } + } + + fetchData(); + }, [location]); + + 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 () => { + setIsLoading(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); + + const credentialTypes = await fetchCredentialTypes(results); + + setCredentials(assignCredentialKinds(results, credentialTypes)); + setCredentialCount(count); + } catch (error) { + setContentError(error); + } + + setIsLoading(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..fee4eee4d4 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { CredentialsAPI, CredentialTypesAPI } from '@api'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { CredentialList } from '.'; +import { mockCredentials, mockCredentialTypes } from '../shared'; + +jest.mock('@api'); + +describe('', () => { + let wrapper; + + beforeEach(async () => { + CredentialsAPI.read.mockResolvedValueOnce({ data: mockCredentials }); + CredentialTypesAPI.read.mockResolvedValueOnce({ + data: mockCredentialTypes, + }); + 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 }); + CredentialTypesAPI.read.mockResolvedValueOnce({ + data: mockCredentialTypes, + }); + 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..2eb56d1bb6 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx @@ -0,0 +1,83 @@ +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.kind}, + + {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..0f23bf465d --- /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.credential_types.json b/awx/ui_next/src/screens/Credential/shared/data.credential_types.json new file mode 100644 index 0000000000..d03480d51c --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/data.credential_types.json @@ -0,0 +1,176 @@ +{ + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "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": "2019-12-16T21:01:03.430100Z", + "modified": "2019-12-16T21:01:24.834078Z", + "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": {} + }, + { + "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": "2019-12-16T21:01:03.443889Z", + "modified": "2019-12-16T21:01:24.844433Z", + "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": 3, + "type": "credential_type", + "url": "/api/v2/credential_types/3/", + "related": { + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "credentials": "/api/v2/credential_types/3/credentials/", + "activity_stream": "/api/v2/credential_types/3/activity_stream/" + }, + "summary_fields": { + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "user_capabilities": { + "edit": true, + "delete": true + } + }, + "created": "2019-12-17T16:11:57.723478Z", + "modified": "2019-12-17T16:12:01.582942Z", + "name": "Bar", + "description": "", + "kind": "cloud", + "namespace": null, + "managed_by_tower": false, + "inputs": {}, + "injectors": {} + } + ] +} \ No newline at end of file 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..7675805040 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/data.credentials.json @@ -0,0 +1,366 @@ +{ + "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": { + "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": { + "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": { + "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": { + "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": { + "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..3cf652f38c --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/index.js @@ -0,0 +1,2 @@ +export { default as mockCredentials } from './data.credentials.json'; +export { default as mockCredentialTypes } from './data.credential_types.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}