diff --git a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx index 5ea13ee6b3..cec9a984ef 100644 --- a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx +++ b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx @@ -1,3 +1,4 @@ +import 'styled-components/macro'; import React from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import { Thead, Tr, Th as PFTh } from '@patternfly/react-table'; @@ -72,6 +73,7 @@ export function HeaderCell({ columnIndex, idPrefix, className, + alignRight, children, }) { const sort = sortKey @@ -86,6 +88,7 @@ export function HeaderCell({ id={sortKey ? `${idPrefix}-${sortKey}` : null} className={className} sort={sort} + css={alignRight ? 'text-align: right' : null} > {children} diff --git a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx index fcfb250a01..f800b33217 100644 --- a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx +++ b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx @@ -89,7 +89,7 @@ function PaginatedTable({ ); } else { Content = ( -
+
{hasContentLoading && } {headerRow} diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx index e4a10ed86c..ced36412f5 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx @@ -7,10 +7,14 @@ import { CredentialsAPI } from '../../../api'; import AlertModal from '../../../components/AlertModal'; import ErrorDetail from '../../../components/ErrorDetail'; import DataListToolbar from '../../../components/DataListToolbar'; -import PaginatedDataList, { +import { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; import useRequest, { useDeleteItems } from '../../../util/useRequest'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import CredentialListItem from './CredentialListItem'; @@ -114,7 +118,7 @@ function CredentialList({ i18n }) { return ( - ( + headerRow={ + + {i18n._(t`Name`)} + {i18n._(t`Type`)} + {i18n._(t`Actions`)} + + } + renderRow={(item, index) => ( row.id === item.id)} onSelect={() => handleSelect(item)} + rowIndex={index} /> )} renderToolbar={props => ( 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 88c52ff228..35bcce1d64 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx @@ -57,25 +57,41 @@ describe('', () => { test('should check and uncheck the row item', async () => { expect( - wrapper.find('DataListCheck[id="select-credential-1"]').props().checked + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .props().checked ).toBe(false); await act(async () => { wrapper - .find('DataListCheck[id="select-credential-1"]') + .find('.pf-c-table__check') + .first() + .find('input') .invoke('onChange')(true); }); wrapper.update(); expect( - wrapper.find('DataListCheck[id="select-credential-1"]').props().checked + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .props().checked ).toBe(true); await act(async () => { wrapper - .find('DataListCheck[id="select-credential-1"]') + .find('.pf-c-table__check') + .first() + .find('input') .invoke('onChange')(false); }); wrapper.update(); expect( - wrapper.find('DataListCheck[id="select-credential-1"]').props().checked + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .props().checked ).toBe(false); }); @@ -105,7 +121,9 @@ describe('', () => { await act(async () => { wrapper - .find('DataListCheck[id="select-credential-3"]') + .find('.pf-c-table__check') + .at(2) + .find('input') .invoke('onChange')(); }); wrapper.update(); @@ -122,7 +140,9 @@ describe('', () => { ); await act(async () => { wrapper - .find('DataListCheck[id="select-credential-2"]') + .find('.pf-c-table__check') + .at(1) + .find('input') .invoke('onChange')(); }); wrapper.update(); diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx index a4e436c073..9886cf3572 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx @@ -3,31 +3,16 @@ import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { PencilAltIcon } from '@patternfly/react-icons'; -import styled from 'styled-components'; -import DataListCell from '../../../components/DataListCell'; +import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { timeOfDay } from '../../../util/dates'; import { Credential } from '../../../types'; import { CredentialsAPI } from '../../../api'; import CopyButton from '../../../components/CopyButton'; -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: repeat(2, 40px); -`; - function CredentialListItem({ credential, detailUrl, @@ -35,6 +20,7 @@ function CredentialListItem({ onSelect, i18n, fetchCredentials, + rowIndex, }) { const [isDisabled, setIsDisabled] = useState(false); @@ -57,64 +43,49 @@ function CredentialListItem({ }, []); return ( - - - - - - {credential.name} - - , - - {credential.summary_fields.credential_type.name} - , - ]} - /> - - {canEdit && ( - - - - )} - {credential.summary_fields.user_capabilities.copy && ( - - )} - - - + + + + + {credential.name} + + + + {credential.summary_fields.credential_type.name} + + + + + + + + + + ); } 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 fcfc085d2d..74bb7e6915 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.test.jsx @@ -16,24 +16,32 @@ describe('', () => { 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(); }); @@ -41,12 +49,16 @@ describe('', () => { CredentialsAPI.copy.mockResolvedValue(); wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
); await act(async () => @@ -60,12 +72,16 @@ describe('', () => { CredentialsAPI.copy.mockRejectedValue(new Error()); wrapper = mountWithContexts( - {}} - credential={mockCredentials.results[0]} - /> + + + {}} + credential={mockCredentials.results[0]} + /> + +
); await act(async () => wrapper.find('Button[aria-label="Copy"]').prop('onClick')() @@ -77,12 +93,16 @@ describe('', () => { test('should not render copy button', async () => { wrapper = mountWithContexts( - {}} - credential={mockCredentials.results[1]} - /> + + + {}} + credential={mockCredentials.results[1]} + /> + +
); expect(wrapper.find('CopyButton').length).toBe(0); }); diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index a1f6d36f41..acb473a34d 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -9,10 +9,14 @@ import useRequest, { useDeleteItems } from '../../../util/useRequest'; import AlertModal from '../../../components/AlertModal'; import DataListToolbar from '../../../components/DataListToolbar'; import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { +import { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; import useWsProjects from './useWsProjects'; import { getQSConfig, parseQueryString } from '../../../util/qs'; @@ -116,7 +120,7 @@ function ProjectList({ i18n }) { - + {i18n._(t`Name`)} + {i18n._(t`Status`)} + {i18n._(t`Type`)} + {i18n._(t`Revision`)} + {i18n._(t`Actions`)} + + } renderToolbar={props => ( )} - renderItem={o => ( + renderRow={(project, index) => ( row.id === o.id)} - onSelect={() => handleSelect(o)} + key={project.id} + project={project} + detailUrl={`${match.url}/${project.id}`} + isSelected={selected.some(row => row.id === project.id)} + onSelect={() => handleSelect(project)} + rowIndex={index} /> )} emptyStateControls={ diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index 1382df00ca..88c72e7f89 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -2,37 +2,22 @@ import 'styled-components/macro'; import React, { Fragment, useState, useCallback } from 'react'; import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; - +import { Button, Tooltip } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; import { PencilAltIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; +import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { formatDateString, timeOfDay } from '../../../util/dates'; import { ProjectsAPI } from '../../../api'; import ClipboardCopyButton from '../../../components/ClipboardCopyButton'; -import StatusIcon from '../../../components/StatusIcon'; -import DataListCell from '../../../components/DataListCell'; +import StatusLabel from '../../../components/StatusLabel'; import { toTitleCase } from '../../../util/strings'; import CopyButton from '../../../components/CopyButton'; import ProjectSyncButton from '../shared/ProjectSyncButton'; import { Project } from '../../../types'; -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: repeat(3, 40px); -`; - const Label = styled.span` color: var(--pf-global--disabled-color--100); `; @@ -42,8 +27,9 @@ function ProjectListItem({ isSelected, onSelect, detailUrl, - i18n, fetchProjects, + rowIndex, + i18n, }) { const [isDisabled, setIsDisabled] = useState(false); ProjectListItem.propTypes = { @@ -88,106 +74,89 @@ function ProjectListItem({ }, []); const labelId = `check-action-${project.id}`; + return ( - - - + + + + {project.name} + + + + {project.summary_fields.last_job && ( + + + + + + )} + + + {project.scm_type === '' + ? i18n._(t`Manual`) + : toTitleCase(project.scm_type)} + + + {project.scm_revision.substring(0, 7)} + {!project.scm_revision && ( + + )} + - - {project.summary_fields.last_job && ( - - - - - - )} - , - - - {project.name} - - , - - {project.scm_type === '' - ? i18n._(t`Manual`) - : toTitleCase(project.scm_type)} - , - - {project.scm_revision.substring(0, 7)} - {!project.scm_revision && ( - - )} - - , - ]} - /> - + + - {project.summary_fields.user_capabilities.start && ( - - - - )} - {project.summary_fields.user_capabilities.edit ? ( - - - - ) : ( - '' - )} - {project.summary_fields.user_capabilities.copy && ( - - )} - - - + + + + + + + + + + ); } export default withI18n()(ProjectListItem); diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx index 7866015703..21f96efc4d 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx @@ -10,112 +10,128 @@ jest.mock('../../../api/models/Projects'); describe('', () => { test('launch button shown to users with start capabilities', () => { const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - start: true, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + start: true, + }, + }, + }} + /> + +
); expect(wrapper.find('ProjectSyncButton').exists()).toBeTruthy(); }); test('launch button hidden from users without start capabilities', () => { const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - start: false, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + start: false, + }, + }, + }} + /> + +
); expect(wrapper.find('ProjectSyncButton').exists()).toBeFalsy(); }); test('edit button shown to users with edit capabilities', () => { const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - edit: true, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + edit: true, + }, + }, + }} + /> + +
); expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); test('edit button hidden from users without edit capabilities', () => { const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - edit: false, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + edit: false, + }, + }, + }} + /> + +
); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); }); @@ -123,29 +139,33 @@ describe('', () => { test('should call api to copy project', async () => { ProjectsAPI.copy.mockResolvedValue(); const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - edit: false, - copy: true, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + edit: false, + copy: true, + }, + }, + }} + /> + +
); await act(async () => @@ -159,29 +179,33 @@ describe('', () => { ProjectsAPI.copy.mockRejectedValue(new Error()); const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - edit: false, - copy: true, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + edit: false, + copy: true, + }, + }, + }} + /> + +
); await act(async () => wrapper.find('Button[aria-label="Copy"]').prop('onClick')() @@ -192,56 +216,64 @@ describe('', () => { }); test('should not render copy button', async () => { const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - edit: false, - copy: false, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + edit: false, + copy: false, + }, + }, + }} + /> + +
); expect(wrapper.find('CopyButton').length).toBe(0); }); test('should render disabled copy to clipboard button', () => { const wrapper = mountWithContexts( - {}} - project={{ - id: 1, - name: 'Project 1', - url: '/api/v2/projects/1', - type: 'project', - scm_type: 'git', - scm_revision: '', - summary_fields: { - last_job: { - id: 9000, - status: 'successful', - }, - user_capabilities: { - edit: true, - }, - }, - }} - /> + + + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + edit: true, + }, + }, + }} + /> + +
); expect( wrapper.find('span[aria-label="copy to clipboard disabled"]').text()