From c4ff27cedbf072168e892d343f29dd016655591b Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Wed, 18 Dec 2019 17:03:52 -0500 Subject: [PATCH 1/4] 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} From eee84b1af7948160261e0613fa28e43a9e5c6c08 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Wed, 18 Dec 2019 19:45:32 -0500 Subject: [PATCH 2/4] Fetch credential types and options only on initial render --- .../CredentialList/CredentialList.jsx | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx index 9881f418dd..11d6b5f79b 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx @@ -20,23 +20,6 @@ const QS_CONFIG = getQSConfig('project', { 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; @@ -55,43 +38,62 @@ function CredentialList({ i18n }) { const [contentError, setContentError] = useState(null); const [credentialCount, setCredentialCount] = useState(0); const [credentials, setCredentials] = useState([]); + const [credentialTypes, setCredentialTypes] = useState(null); const [deletionError, setDeletionError] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [hasContentLoading, setHasContentLoading] = useState(true); const [selected, setSelected] = useState([]); const location = useLocation(); - useEffect(() => { - async function fetchData() { - const params = parseQueryString(QS_CONFIG, location.search); + const loadCredentials = async ({ search }) => { + const params = parseQueryString(QS_CONFIG, search); + setContentError(null); + setHasContentLoading(true); + try { + const [ + { + data: { count, results }, + }, + { + data: { actions: optionActions }, + }, + { + data: { results: credentialTypeResults }, + }, + ] = await Promise.all([ + CredentialsAPI.read(params), + loadCredentialActions(), + loadCredentialTypes(), + ]); - 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); - } + setActions(optionActions); + setCredentialCount(count); + setCredentials(assignCredentialKinds(results, credentialTypeResults)); + setCredentialTypes(credentialTypeResults); + } catch (error) { + setContentError(error); + } finally { + setHasContentLoading(false); } + }; - fetchData(); - }, [location]); + useEffect(() => { + loadCredentials(location); + }, [location]); // eslint-disable-line react-hooks/exhaustive-deps + + const loadCredentialTypes = () => { + if (credentialTypes) { + return Promise.resolve({ data: { results: credentialTypes } }); + } + return CredentialTypesAPI.read({ page_size: 200 }); + }; + + const loadCredentialActions = () => { + if (actions) { + return Promise.resolve({ data: { actions } }); + } + return CredentialsAPI.readOptions(); + }; const handleSelectAll = isSelected => { setSelected(isSelected ? [...credentials] : []); @@ -106,7 +108,7 @@ function CredentialList({ i18n }) { }; const handleDelete = async () => { - setIsLoading(true); + setHasContentLoading(true); try { await Promise.all( @@ -122,15 +124,13 @@ function CredentialList({ i18n }) { data: { count, results }, } = await CredentialsAPI.read(params); - const credentialTypes = await fetchCredentialTypes(results); - setCredentials(assignCredentialKinds(results, credentialTypes)); setCredentialCount(count); } catch (error) { setContentError(error); } - setIsLoading(false); + setHasContentLoading(false); }; const canAdd = @@ -143,7 +143,7 @@ function CredentialList({ i18n }) { Date: Thu, 19 Dec 2019 10:23:19 -0500 Subject: [PATCH 3/4] Use credential_types from credential.summary_fields to display "Type" column --- .../CredentialList/CredentialList.jsx | 36 +--- .../CredentialList/CredentialList.test.jsx | 10 +- .../CredentialList/CredentialListItem.jsx | 4 +- .../CredentialListItem.test.jsx | 2 +- .../shared/data.credential_types.json | 176 ------------------ .../Credential/shared/data.credentials.json | 25 +++ .../src/screens/Credential/shared/index.js | 3 +- 7 files changed, 37 insertions(+), 219 deletions(-) delete mode 100644 awx/ui_next/src/screens/Credential/shared/data.credential_types.json diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx index 11d6b5f79b..6163844b27 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx @@ -2,7 +2,7 @@ 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 { CredentialsAPI } from '@api'; import { Card, PageSection } from '@patternfly/react-core'; import AlertModal from '@components/AlertModal'; import ErrorDetail from '@components/ErrorDetail'; @@ -14,31 +14,17 @@ import PaginatedDataList, { import { getQSConfig, parseQueryString } from '@util/qs'; import { CredentialListItem } from '.'; -const QS_CONFIG = getQSConfig('project', { +const QS_CONFIG = getQSConfig('credential', { page: 1, page_size: 20, order_by: 'name', }); -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 [credentialTypes, setCredentialTypes] = useState(null); const [deletionError, setDeletionError] = useState(null); const [hasContentLoading, setHasContentLoading] = useState(true); const [selected, setSelected] = useState([]); @@ -57,19 +43,14 @@ function CredentialList({ i18n }) { { data: { actions: optionActions }, }, - { - data: { results: credentialTypeResults }, - }, ] = await Promise.all([ CredentialsAPI.read(params), loadCredentialActions(), - loadCredentialTypes(), ]); - setActions(optionActions); + setCredentials(results); setCredentialCount(count); - setCredentials(assignCredentialKinds(results, credentialTypeResults)); - setCredentialTypes(credentialTypeResults); + setActions(optionActions); } catch (error) { setContentError(error); } finally { @@ -81,13 +62,6 @@ function CredentialList({ i18n }) { loadCredentials(location); }, [location]); // eslint-disable-line react-hooks/exhaustive-deps - const loadCredentialTypes = () => { - if (credentialTypes) { - return Promise.resolve({ data: { results: credentialTypes } }); - } - return CredentialTypesAPI.read({ page_size: 200 }); - }; - const loadCredentialActions = () => { if (actions) { return Promise.resolve({ data: { actions } }); @@ -124,7 +98,7 @@ function CredentialList({ i18n }) { data: { count, results }, } = await CredentialsAPI.read(params); - setCredentials(assignCredentialKinds(results, credentialTypes)); + setCredentials(results); setCredentialCount(count); } catch (error) { setContentError(error); diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx index fee4eee4d4..fc45442a5d 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx @@ -1,9 +1,9 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { CredentialsAPI, CredentialTypesAPI } from '@api'; +import { CredentialsAPI } from '@api'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { CredentialList } from '.'; -import { mockCredentials, mockCredentialTypes } from '../shared'; +import mockCredentials from '../shared'; jest.mock('@api'); @@ -12,9 +12,6 @@ describe('', () => { beforeEach(async () => { CredentialsAPI.read.mockResolvedValueOnce({ data: mockCredentials }); - CredentialTypesAPI.read.mockResolvedValueOnce({ - data: mockCredentialTypes, - }); CredentialsAPI.readOptions.mockResolvedValue({ data: { actions: { @@ -101,9 +98,6 @@ describe('', () => { 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 () => { diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx index 2eb56d1bb6..ccb58269b0 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx @@ -52,7 +52,9 @@ function CredentialListItem({ {credential.name} , - {credential.kind}, + + {credential.summary_fields.credential_type.name} + , {canEdit && ( diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx index 0f23bf465d..bb6e62dc6c 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { CredentialListItem } from '.'; -import { mockCredentials } from '../shared'; +import mockCredentials from '../shared'; describe('', () => { let wrapper; 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 deleted file mode 100644 index d03480d51c..0000000000 --- a/awx/ui_next/src/screens/Credential/shared/data.credential_types.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "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 index 7675805040..60d4b71d05 100644 --- a/awx/ui_next/src/screens/Credential/shared/data.credentials.json +++ b/awx/ui_next/src/screens/Credential/shared/data.credentials.json @@ -21,6 +21,11 @@ "user": "/api/v2/users/7/" }, "summary_fields": { + "credential_type": { + "id": 1, + "name": "Machine", + "description": "" + }, "created_by": { "id": 1, "username": "admin", @@ -84,6 +89,11 @@ "user": "/api/v2/users/7/" }, "summary_fields": { + "credential_type": { + "id": 1, + "name": "Machine", + "description": "" + }, "created_by": { "id": 8, "username": "user-2", @@ -158,6 +168,11 @@ "user": "/api/v2/users/8/" }, "summary_fields": { + "credential_type": { + "id": 1, + "name": "Machine", + "description": "" + }, "created_by": { "id": 9, "username": "user-3", @@ -232,6 +247,11 @@ "user": "/api/v2/users/1/" }, "summary_fields": { + "credential_type": { + "id": 2, + "name": "Vault", + "description": "" + }, "created_by": { "id": 1, "username": "admin", @@ -306,6 +326,11 @@ "user": "/api/v2/users/1/" }, "summary_fields": { + "credential_type": { + "id": 3, + "name": "Source Control", + "description": "" + }, "created_by": { "id": 1, "username": "admin", diff --git a/awx/ui_next/src/screens/Credential/shared/index.js b/awx/ui_next/src/screens/Credential/shared/index.js index 3cf652f38c..723f2ff91e 100644 --- a/awx/ui_next/src/screens/Credential/shared/index.js +++ b/awx/ui_next/src/screens/Credential/shared/index.js @@ -1,2 +1 @@ -export { default as mockCredentials } from './data.credentials.json'; -export { default as mockCredentialTypes } from './data.credential_types.json'; +export { default } from './data.credentials.json'; From 6c3e42a1ac6bb9067367454e8db025047c5f53ca Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 19 Dec 2019 13:41:40 -0500 Subject: [PATCH 4/4] Reset selected list after successful delete --- .../src/screens/Credential/CredentialList/CredentialList.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx index 6163844b27..ee94b492c8 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx @@ -100,6 +100,7 @@ function CredentialList({ i18n }) { setCredentials(results); setCredentialCount(count); + setSelected([]); } catch (error) { setContentError(error); }