diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index 4bdba17027..4422dc8c01 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -6,7 +6,7 @@ import { t } from '@lingui/macro'; import AlertModal from '../AlertModal'; import ErrorDetail from '../ErrorDetail'; import NotificationListItem from './NotificationListItem'; -import PaginatedDataList from '../PaginatedDataList'; +import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable'; import { getQSConfig, parseQueryString } from '../../util/qs'; import useRequest from '../../util/useRequest'; import { NotificationTemplatesAPI } from '../../api'; @@ -169,7 +169,7 @@ function NotificationList({ return ( <> - ( + headerRow={ + + {i18n._(t`Name`)} + + {i18n._(t`Type`)} + + {i18n._(t`Options`)} + + } + renderRow={(notification, index) => ( )} /> diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx index 071eb45e9f..720b2f15c0 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx @@ -87,10 +87,6 @@ describe('', () => { wrapper.unmount(); }); - test('initially renders succesfully', () => { - expect(wrapper.find('PaginatedDataList')).toHaveLength(1); - }); - test('should render list fetched of items', () => { expect(NotificationTemplatesAPI.read).toHaveBeenCalled(); expect(NotificationTemplatesAPI.readOptions).toHaveBeenCalled(); diff --git a/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx b/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx index 8419db0977..5b0fc2fad7 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx @@ -3,25 +3,9 @@ import { shape, number, string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { - DataListAction as _DataListAction, - DataListItem, - DataListItemCells, - DataListItemRow, - Switch, -} from '@patternfly/react-core'; -import styled from 'styled-components'; -import DataListCell from '../DataListCell'; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: ${props => `repeat(${props.columns}, max-content)`}; -`; -const Label = styled.b` - margin-right: 20px; -`; +import { Switch } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; +import { ActionsTd, ActionItem } from '../PaginatedTable'; function NotificationListItem({ canToggleNotifications, @@ -37,54 +21,37 @@ function NotificationListItem({ showApprovalsToggle, }) { return ( - - - - - - {notification.name} - - - , - - - {typeLabels[notification.notification_type]} - , - ]} - /> - - {showApprovalsToggle && ( - - toggleNotification( - notification.id, - approvalsTurnedOn, - 'approvals' - ) - } - aria-label={i18n._(t`Toggle notification approvals`)} - /> - )} + + + + {notification.name} + + + + {typeLabels[notification.notification_type]} + + + + + toggleNotification( + notification.id, + approvalsTurnedOn, + 'approvals' + ) + } + aria-label={i18n._(t`Toggle notification approvals`)} + /> + + + + + + - - - + + + ); } diff --git a/awx/ui_next/src/components/NotificationList/NotificationListItem.test.jsx b/awx/ui_next/src/components/NotificationList/NotificationListItem.test.jsx index 22afb5373b..983525db04 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationListItem.test.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationListItem.test.jsx @@ -30,13 +30,17 @@ describe('', () => { test('initially renders succesfully and displays correct label', () => { wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('NotificationListItem')).toMatchSnapshot(); expect(wrapper.find('Switch').length).toBe(3); @@ -44,46 +48,55 @@ describe('', () => { test('shows approvals toggle when configured', () => { wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('Switch').length).toBe(4); }); - test('displays correct label in correct column', () => { + test('displays correct type', () => { wrapper = mountWithContexts( - + + + + +
); - const typeCell = wrapper - .find('DataListCell') - .at(1) - .find('div'); + const typeCell = wrapper.find('Td').at(1); expect(typeCell.text()).toContain('Slack'); }); test('handles approvals click when toggle is on', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification approvals"]') @@ -95,15 +108,19 @@ describe('', () => { test('handles approvals click when toggle is off', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification approvals"]') @@ -114,14 +131,18 @@ describe('', () => { test('handles started click when toggle is on', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification start"]') @@ -132,14 +153,18 @@ describe('', () => { test('handles started click when toggle is off', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification start"]') @@ -150,14 +175,18 @@ describe('', () => { test('handles success click when toggle is on', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification success"]') @@ -168,14 +197,18 @@ describe('', () => { test('handles success click when toggle is off', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification success"]') @@ -186,14 +219,18 @@ describe('', () => { test('handles error click when toggle is on', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification failure"]') @@ -204,14 +241,18 @@ describe('', () => { test('handles error click when toggle is off', () => { wrapper = mountWithContexts( - + + + + +
); wrapper .find('Switch[aria-label="Toggle notification failure"]') diff --git a/awx/ui_next/src/components/NotificationList/__snapshots__/NotificationListItem.test.jsx.snap b/awx/ui_next/src/components/NotificationList/__snapshots__/NotificationListItem.test.jsx.snap index 7303fdbd7d..eaf045d78d 100644 --- a/awx/ui_next/src/components/NotificationList/__snapshots__/NotificationListItem.test.jsx.snap +++ b/awx/ui_next/src/components/NotificationList/__snapshots__/NotificationListItem.test.jsx.snap @@ -24,398 +24,442 @@ exports[` initially renders succe } } > - -
  • - -
  • -
    + + + + +
    `; diff --git a/awx/ui_next/src/components/PaginatedTable/ActionItem.jsx b/awx/ui_next/src/components/PaginatedTable/ActionItem.jsx index f9c423fee3..5d4f22d12f 100644 --- a/awx/ui_next/src/components/PaginatedTable/ActionItem.jsx +++ b/awx/ui_next/src/components/PaginatedTable/ActionItem.jsx @@ -13,9 +13,13 @@ export default function ActionItem({ column, tooltip, visible, children }) { grid-column: ${column}; `} > - -
    {children}
    -
    + {tooltip ? ( + +
    {children}
    +
    + ) : ( + children + )} ); } diff --git a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx index cec9a984ef..a7b076da57 100644 --- a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx +++ b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx @@ -13,7 +13,12 @@ const Th = styled(PFTh)` --pf-c-table--cell--Overflow: initial; `; -export default function HeaderRow({ qsConfig, isExpandable, children }) { +export default function HeaderRow({ + qsConfig, + isExpandable, + isSelectable, + children, +}) { const location = useLocation(); const history = useHistory(); @@ -49,7 +54,7 @@ export default function HeaderRow({ qsConfig, isExpandable, children }) { {isExpandable && } - + {isSelectable && } {React.Children.map( children, child => @@ -66,6 +71,10 @@ export default function HeaderRow({ qsConfig, isExpandable, children }) { ); } +HeaderRow.defaultProps = { + isSelectable: true, +}; + export function HeaderCell({ sortKey, onSort, diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationList.test.jsx b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationList.test.jsx index 7824ea5ed5..6832d07021 100644 --- a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationList.test.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationList.test.jsx @@ -48,6 +48,7 @@ describe('', () => { }); await waitForElement(wrapper, 'ApplicationsList', el => el.length > 0); }); + test('should have data fetched and render 2 rows', async () => { ApplicationsAPI.read.mockResolvedValue(applications); ApplicationsAPI.readOptions.mockResolvedValue(options); @@ -69,14 +70,20 @@ describe('', () => { waitForElement(wrapper, 'ApplicationsList', el => el.length > 0); wrapper - .find('input#select-application-1') + .find('.pf-c-table__check') + .first() + .find('input') .simulate('change', applications.data.results[0]); wrapper.update(); - expect(wrapper.find('input#select-application-1').prop('checked')).toBe( - true - ); + expect( + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') + ).toBe(true); await act(async () => wrapper.find('Button[aria-label="Delete"]').prop('onClick')() ); @@ -131,13 +138,21 @@ describe('', () => { }); waitForElement(wrapper, 'ApplicationsList', el => el.length > 0); - wrapper.find('input#select-application-1').simulate('change', 'a'); + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .simulate('change', 'a'); wrapper.update(); - expect(wrapper.find('input#select-application-1').prop('checked')).toBe( - true - ); + expect( + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') + ).toBe(true); await act(async () => wrapper.find('Button[aria-label="Delete"]').prop('onClick')() ); @@ -163,6 +178,7 @@ describe('', () => { waitForElement(wrapper, 'ApplicationsList', el => el.length > 0); expect(wrapper.find('ToolbarAddButton').length).toBe(0); }); + test('should not render edit button for first list item', async () => { applications.data.results[0].summary_fields.user_capabilities.edit = false; ApplicationsAPI.read.mockResolvedValue(applications); diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx index 1a0f5c9f4e..0d8a10b98e 100644 --- a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx @@ -1,104 +1,65 @@ import React from 'react'; import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemCells, - DataListItemRow, - Tooltip, -} from '@patternfly/react-core'; - +import { Button } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import styled from 'styled-components'; import { PencilAltIcon } from '@patternfly/react-icons'; +import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { formatDateString } from '../../../util/dates'; import { Application } from '../../../types'; -import DataListCell from '../../../components/DataListCell'; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: 40px; -`; - -const Label = styled.b` - margin-right: 20px; -`; function ApplicationListItem({ application, isSelected, onSelect, detailUrl, + rowIndex, i18n, }) { const labelId = `check-action-${application.id}`; return ( - - - - - - {application.name} - - , - - - {application.summary_fields.organization.name} - - , - - - {formatDateString(application.modified)} - , - ]} - /> - + + + + {application.name} + + + + - {application.summary_fields.user_capabilities.edit ? ( - - - - ) : ( - '' - )} - - - + {application.summary_fields.organization.name} + + + + {formatDateString(application.modified)} + + + + + + + ); } diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.test.jsx b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.test.jsx index 0a53dd4cd8..510c89ff28 100644 --- a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.test.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.test.jsx @@ -18,12 +18,16 @@ describe('', () => { test('should mount successfully', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); expect(wrapper.find('ApplicationListItem').length).toBe(1); @@ -31,38 +35,30 @@ describe('', () => { test('should render the proper data', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); expect( - wrapper.find('DataListCell[aria-label="application name"]').text() + wrapper + .find('Td') + .at(1) + .text() ).toBe('Foo'); expect( - wrapper.find('DataListCell[aria-label="organization name"]').text() + wrapper + .find('Td') + .at(2) + .text() ).toBe('Organization'); - expect(wrapper.find('input#select-application-1').prop('checked')).toBe( - false - ); expect(wrapper.find('PencilAltIcon').length).toBe(1); }); - test('should be checked', async () => { - await act(async () => { - wrapper = mountWithContexts( - {}} - /> - ); - }); - expect(wrapper.find('input#select-application-1').prop('checked')).toBe( - true - ); - }); }); diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx index b35ecc7d68..5ed5e092da 100644 --- a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx @@ -11,7 +11,11 @@ import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; import { ApplicationsAPI } from '../../../api'; -import PaginatedDataList, { +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; +import { ToolbarDeleteButton, ToolbarAddButton, } from '../../../components/PaginatedDataList'; @@ -104,7 +108,7 @@ function ApplicationsList({ i18n }) { <> - ( @@ -170,7 +156,17 @@ function ApplicationsList({ i18n }) { ]} /> )} - renderItem={application => ( + headerRow={ + + {i18n._(t`Name`)} + + {i18n._(t`Organization`)} + + {i18n._(t`Last Modified`)} + {i18n._(t`Actions`)} + + } + renderRow={(application, index) => ( handleSelect(application)} isSelected={selected.some(row => row.id === application.id)} + rowIndex={index} /> )} emptyStateControls={ diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx index ee47d2fdbc..7c491bd36a 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx @@ -8,10 +8,14 @@ import { CredentialTypesAPI } from '../../../api'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useSelected from '../../../util/useSelected'; -import PaginatedDataList, { +import { ToolbarDeleteButton, ToolbarAddButton, } from '../../../components/PaginatedDataList'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; import ErrorDetail from '../../../components/ErrorDetail'; import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; @@ -106,7 +110,7 @@ function CredentialTypeList({ i18n }) { <> - )} - renderItem={credentialType => ( + headerRow={ + + {i18n._(t`Name`)} + {i18n._(t`Actions`)} + + } + renderRow={(credentialType, index) => ( handleSelect(credentialType)} isSelected={selected.some(row => row.id === credentialType.id)} + rowIndex={index} /> )} emptyStateControls={ diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx index adeda5255b..16f5f4daf7 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx @@ -72,12 +72,18 @@ describe(' { await waitForElement(wrapper, 'CredentialTypeList', el => el.length > 0); wrapper - .find('input#select-credential-types-1') + .find('.pf-c-table__check') + .first() + .find('input') .simulate('change', credentialTypes.data.results[0]); wrapper.update(); expect( - wrapper.find('input#select-credential-types-1').prop('checked') + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') ).toBe(true); await act(async () => { @@ -133,10 +139,18 @@ describe(' { }); waitForElement(wrapper, 'CredentialTypeList', el => el.length > 0); - wrapper.find('input#select-credential-types-1').simulate('change', 'a'); + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .simulate('change', 'a'); wrapper.update(); expect( - wrapper.find('input#select-credential-types-1').prop('checked') + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') ).toBe(true); await act(async () => diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx index d6dd1cf1fa..ea461b58bb 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx @@ -3,82 +3,53 @@ 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 { CredentialType } from '../../../types'; -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: 40px; -`; - function CredentialTypeListItem({ credentialType, detailUrl, isSelected, onSelect, + rowIndex, i18n, }) { const labelId = `check-action-${credentialType.id}`; return ( - - - - - - {credentialType.name} - - , - ]} - /> - + + + + {credentialType.name} + + + + - {credentialType.summary_fields.user_capabilities.edit && ( - - - - )} - - - + + + + ); } diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.test.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.test.jsx index 3cb4f6cc52..45c78d918e 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.test.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.test.jsx @@ -17,12 +17,16 @@ describe('', () => { test('should mount successfully', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); expect(wrapper.find('CredentialTypeListItem').length).toBe(1); @@ -31,48 +35,38 @@ describe('', () => { test('should render the proper data', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); - expect( - wrapper.find('DataListCell[aria-label="credential type name"]').text() - ).toBe('Foo'); + expect(wrapper.find('Td[dataLabel="Name"]').text()).toBe('Foo'); expect(wrapper.find('PencilAltIcon').length).toBe(1); - expect( - wrapper.find('input#select-credential-types-1').prop('checked') - ).toBe(false); - }); - - test('should be checked', async () => { - await act(async () => { - wrapper = mountWithContexts( - {}} - /> - ); - }); - expect( - wrapper.find('input#select-credential-types-1').prop('checked') - ).toBe(true); + expect(wrapper.find('.pf-c-table__check input').prop('checked')).toBe( + undefined + ); }); test('edit button shown to users with edit capabilities', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); @@ -82,15 +76,19 @@ describe('', () => { test('edit button hidden from users without edit capabilities', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx index 6b3b43f637..5e1994f640 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx @@ -8,9 +8,11 @@ import { InstanceGroupsAPI } from '../../../api'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useSelected from '../../../util/useSelected'; -import PaginatedDataList, { - ToolbarDeleteButton, -} from '../../../components/PaginatedDataList'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; +import { ToolbarDeleteButton } from '../../../components/PaginatedDataList'; import ErrorDetail from '../../../components/ErrorDetail'; import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; @@ -189,7 +191,7 @@ function InstanceGroupList({ i18n }) { <> - )} - renderItem={instanceGroup => ( + headerRow={ + + {i18n._(t`Name`)} + {i18n._(t`Type`)} + {i18n._(t`Running Jobs`)} + {i18n._(t`Total Jobs`)} + {i18n._(t`Instances`)} + {i18n._(t`Capacity`)} + {i18n._(t`Actions`)} + + } + renderRow={(instanceGroup, index) => ( handleSelect(instanceGroup)} isSelected={selected.some(row => row.id === instanceGroup.id)} + rowIndex={index} /> )} emptyStateControls={canAdd && addButton} diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx index 335089e4bc..74f397dc1c 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx @@ -71,13 +71,19 @@ describe('', () => { await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0); wrapper - .find('input#select-instance-groups-1') + .find('.pf-c-table__check') + .first() + .find('input') .simulate('change', instanceGroups); wrapper.update(); - expect(wrapper.find('input#select-instance-groups-1').prop('checked')).toBe( - true - ); + expect( + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') + ).toBe(true); await act(async () => { wrapper.find('Button[aria-label="Delete"]').prop('onClick')(); @@ -102,16 +108,22 @@ describe('', () => { }); await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0); - const instanceGroupIndex = [1, 2, 3]; + const instanceGroupIndex = [0, 1, 2]; instanceGroupIndex.forEach(element => { wrapper - .find(`input#select-instance-groups-${element}`) + .find('.pf-c-table__check') + .at(element) + .find('input') .simulate('change', instanceGroups); wrapper.update(); expect( - wrapper.find(`input#select-instance-groups-${element}`).prop('checked') + wrapper + .find('.pf-c-table__check') + .at(element) + .find('input') + .prop('checked') ).toBe(true); }); @@ -159,11 +171,19 @@ describe('', () => { }); waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0); - wrapper.find('input#select-instance-groups-1').simulate('change', 'a'); + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .simulate('change', 'a'); wrapper.update(); - expect(wrapper.find('input#select-instance-groups-1').prop('checked')).toBe( - true - ); + expect( + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') + ).toBe(true); await act(async () => wrapper.find('Button[aria-label="Delete"]').prop('onClick')() diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 670835c405..8bfcf05325 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -5,48 +5,18 @@ import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; import 'styled-components/macro'; import { - Badge as PFBadge, Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemCells, - DataListItemRow, Label, Progress, ProgressMeasureLocation, ProgressSize, - Tooltip, } 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 { InstanceGroup } from '../../../types'; -const DataListCell = styled(_DataListCell)` - white-space: nowrap; -`; - -const Badge = styled(PFBadge)` - margin-left: 8px; -`; - -const ListGroup = styled.span` - margin-left: 12px; - - &:first-of-type { - margin-left: 0; - } -`; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: 40px; -`; - const Unavailable = styled.span` color: var(--pf-global--danger-color--200); `; @@ -56,6 +26,7 @@ function InstanceGroupListItem({ detailUrl, isSelected, onSelect, + rowIndex, i18n, }) { const labelId = `check-action-${instanceGroup.id}`; @@ -104,98 +75,50 @@ function InstanceGroupListItem({ }; return ( - - - - - - - - {instanceGroup.name} - - - {verifyInstanceGroup(instanceGroup)} - , - - - {i18n._(t`Type`)} - - {isContainerGroup(instanceGroup) - ? i18n._(t`Container group`) - : i18n._(t`Instance group`)} - - , - - - {i18n._(t`Running jobs`)} - {instanceGroup.jobs_running} - - - {i18n._(t`Total jobs`)} - {instanceGroup.jobs_total} - - - {!instanceGroup.is_containerized ? ( - - {i18n._(t`Instances`)} - {instanceGroup.instances} - - ) : null} - , - - - {usedCapacity(instanceGroup)} - , - ]} - /> - + + + + {instanceGroup.name} + {verifyInstanceGroup(instanceGroup)} + + + + {isContainerGroup(instanceGroup) + ? i18n._(t`Container group`) + : i18n._(t`Instance group`)} + + {instanceGroup.jobs_running} + {instanceGroup.jobs_total} + {instanceGroup.instances} + {usedCapacity(instanceGroup)} + + - {instanceGroup.summary_fields.user_capabilities.edit && ( - - - - )} - - - + + + + ); } InstanceGroupListItem.prototype = { diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx index 9c819dd964..0f22a4b6d7 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.test.jsx @@ -47,12 +47,16 @@ describe('', () => { test('should mount successfully', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); expect(wrapper.find('InstanceGroupListItem').length).toBe(1); @@ -61,73 +65,81 @@ describe('', () => { test('should render the proper data instance group', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); expect( - wrapper.find('PFDataListCell[aria-label="instance group name"]').text() + wrapper + .find('Td') + .at(1) + .text() ).toBe('Foo'); expect(wrapper.find('Progress').prop('value')).toBe(40); expect( - wrapper.find('PFDataListCell[aria-label="instance group type"]').text() - ).toBe('TypeInstance group'); + wrapper + .find('Td') + .at(2) + .text() + ).toBe('Instance group'); expect(wrapper.find('PencilAltIcon').length).toBe(1); - expect(wrapper.find('input#select-instance-groups-1').prop('checked')).toBe( - false + expect(wrapper.find('.pf-c-table__check input').prop('checked')).toBe( + undefined ); }); test('should render the proper data container group', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); expect( - wrapper.find('PFDataListCell[aria-label="instance group name"]').text() + wrapper + .find('Td') + .at(1) + .text() ).toBe('Bar'); expect( - wrapper.find('PFDataListCell[aria-label="instance group type"]').text() - ).toBe('TypeContainer group'); + wrapper + .find('Td') + .at(2) + .text() + ).toBe('Container group'); expect(wrapper.find('PencilAltIcon').length).toBe(0); }); - test('should be checked', async () => { - await act(async () => { - wrapper = mountWithContexts( - {}} - /> - ); - }); - expect(wrapper.find('input#select-instance-groups-1').prop('checked')).toBe( - true - ); - }); - test('edit button shown to users with edit capabilities', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); @@ -137,12 +149,16 @@ describe('', () => { test('edit button hidden from users without edit capabilities', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
    ); }); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx index a377e73e79..c1b277b730 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx @@ -4,7 +4,11 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Card, PageSection } from '@patternfly/react-core'; import { NotificationTemplatesAPI } from '../../../api'; -import PaginatedDataList, { +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; +import { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; @@ -104,7 +108,7 @@ function NotificationTemplatesList({ i18n }) { <> - ( )} - renderItem={template => ( + headerRow={ + + {i18n._(t`Name`)} + {i18n._(t`Status`)} + + {i18n._(t`Type`)} + + {i18n._(t`Actions`)} + + } + renderRow={(template, index) => ( row.id === template.id)} onSelect={() => handleSelect(template)} + rowIndex={index} /> )} emptyStateControls={ diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx index d39bffe087..345d2d0a42 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx @@ -89,21 +89,33 @@ describe('', () => { }); test('should select item', async () => { - const itemCheckboxInput = 'input#select-template-1'; await act(async () => { wrapper = mountWithContexts(); }); wrapper.update(); - expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(false); + expect( + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') + ).toEqual(false); await act(async () => { wrapper - .find(itemCheckboxInput) - .closest('DataListCheck') + .find('.pf-c-table__check') + .first() + .find('input') .props() .onChange(); }); wrapper.update(); - expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(true); + expect( + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .prop('checked') + ).toEqual(true); }); test('should delete notifications', async () => { @@ -135,7 +147,6 @@ describe('', () => { }); test('should show error dialog shown for failed deletion', async () => { - const itemCheckboxInput = 'input#select-template-1'; OrganizationsAPI.destroy.mockRejectedValue( new Error({ response: { @@ -153,8 +164,9 @@ describe('', () => { wrapper.update(); await act(async () => { wrapper - .find(itemCheckboxInput) - .closest('DataListCheck') + .find('.pf-c-table__check') + .first() + .find('input') .props() .onChange(); }); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index f3386f12a9..93d1f82999 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -3,32 +3,17 @@ import React, { useState, useEffect, useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import styled from 'styled-components'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemCells, - DataListItemRow, - Tooltip, -} from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; +import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { timeOfDay } from '../../../util/dates'; import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api'; -import DataListCell from '../../../components/DataListCell'; import StatusLabel from '../../../components/StatusLabel'; import CopyButton from '../../../components/CopyButton'; import useRequest from '../../../util/useRequest'; import { NOTIFICATION_TYPES } from '../constants'; -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: repeat(3, 40px); -`; - const NUM_RETRIES = 25; const RETRY_TIMEOUT = 5000; @@ -38,6 +23,7 @@ function NotificationTemplateListItem({ fetchTemplates, isSelected, onSelect, + rowIndex, i18n, }) { const recentNotifications = template.summary_fields?.recent_notifications; @@ -102,76 +88,65 @@ function NotificationTemplateListItem({ const labelId = `template-name-${template.id}`; return ( - - - - - - {template.name} - - , - - {status && } - , - - {i18n._(t`Type:`)}{' '} - {NOTIFICATION_TYPES[template.notification_type] || - template.notification_type} - , - ]} - /> - + + + + {template.name} + + + + {status && } + + + {NOTIFICATION_TYPES[template.notification_type] || + template.notification_type} + + + + + + - - - - {template.summary_fields.user_capabilities.edit ? ( - - - - ) : ( -
    - )} - {template.summary_fields.user_capabilities.copy && ( - - - - )} - - - + + + + + + + ); } diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx index 65f959522c..2bdcde637f 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx @@ -26,17 +26,21 @@ const template = { describe('', () => { test('should render template row', () => { const wrapper = mountWithContexts( - + + + + +
    ); - const cells = wrapper.find('DataListCell'); - expect(cells).toHaveLength(3); - expect(cells.at(0).text()).toEqual('Test Notification'); - expect(cells.at(1).text()).toEqual('Success'); - expect(cells.at(2).text()).toEqual('Type: Slack'); + const cells = wrapper.find('Td'); + expect(cells).toHaveLength(5); + expect(cells.at(1).text()).toEqual('Test Notification'); + expect(cells.at(2).text()).toEqual('Success'); + expect(cells.at(3).text()).toEqual('Slack'); }); test('should send test notification', async () => { @@ -45,10 +49,14 @@ describe('', () => { }); const wrapper = mountWithContexts( - + + + + +
    ); await act(async () => { wrapper @@ -59,8 +67,8 @@ describe('', () => { expect(NotificationTemplatesAPI.test).toHaveBeenCalledTimes(1); expect( wrapper - .find('DataListCell') - .at(1) + .find('Td') + .at(2) .text() ).toEqual('Running'); }); @@ -69,10 +77,14 @@ describe('', () => { NotificationTemplatesAPI.copy.mockResolvedValue(); const wrapper = mountWithContexts( - + + + + +
    ); await act(async () => @@ -86,10 +98,14 @@ describe('', () => { NotificationTemplatesAPI.copy.mockRejectedValue(new Error()); const wrapper = mountWithContexts( - + + + + +
    ); await act(async () => wrapper.find('Button[aria-label="Copy"]').prop('onClick')() @@ -101,18 +117,22 @@ describe('', () => { test('should not render copy button', async () => { const wrapper = mountWithContexts( - + + + + +
    ); expect(wrapper.find('CopyButton').length).toBe(0); });