From 953fa3fe0d52a3dff70a90759fe0b4fe7f6a2aae Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 6 Jan 2021 11:18:53 -0800 Subject: [PATCH] convert Schedules list to PaginatedTable --- awx/ui_next/.eslintrc | 75 +++++++++- .../components/PaginatedTable/ActionsTd.jsx | 6 +- .../Schedule/ScheduleList/ScheduleList.jsx | 28 ++-- .../ScheduleList/ScheduleList.test.jsx | 39 +++-- .../ScheduleList/ScheduleListItem.jsx | 134 +++++++----------- .../ScheduleList/ScheduleListItem.test.jsx | 86 ++++++----- 6 files changed, 222 insertions(+), 146 deletions(-) diff --git a/awx/ui_next/.eslintrc b/awx/ui_next/.eslintrc index c93c2a8992..c464701811 100644 --- a/awx/ui_next/.eslintrc +++ b/awx/ui_next/.eslintrc @@ -8,8 +8,14 @@ "modules": true } }, - "plugins": ["react-hooks", "jsx-a11y", "i18next"], - "extends": ["airbnb", "prettier", "prettier/react", "plugin:jsx-a11y/strict", "plugin:i18next/recommended"], + "plugins": ["react-hooks", "jsx-a11y", "i18next"], + "extends": [ + "airbnb", + "prettier", + "prettier/react", + "plugin:jsx-a11y/strict", + "plugin:i18next/recommended" + ], "settings": { "react": { "version": "16.5.2" @@ -24,7 +30,70 @@ "window": true }, "rules": { - "i18next/no-literal-string": [2, {"markupOnly": true, "ignoreAttribute": ["to", "streamType", "path", "component", "variant", "key", "position", "promptName", "color","promptId", "headingLevel", "size", "target", "autoComplete","trigger", "from", "name", "fieldId", "css", "gutter", "dataCy", "tooltipMaxWidth", "mode", "aria-labelledby","aria-hidden","sortKey", "ouiaId", "credentialTypeNamespace", "link", "value", "credentialTypeKind", "linkTo", "scrollToAlignment", "displayKey", "sortedColumnKey", "maxHeight", "role", "aria-haspopup", "dropDirection", "resizeOrientation", "src", "theme"], "ignore":["Ansible", "Tower", "JSON", "YAML", "lg", "START"],"ignoreComponent":["code", "Omit","PotentialLink", "TypeRedirect", "Radio", "RunOnRadio", "NodeTypeLetter", "SelectableItem", "Dash"], "ignoreCallee": ["describe"] }], + "i18next/no-literal-string": [ + 2, + { + "markupOnly": true, + "ignoreAttribute": [ + "to", + "streamType", + "path", + "component", + "variant", + "key", + "position", + "promptName", + "color", + "promptId", + "headingLevel", + "size", + "target", + "autoComplete", + "trigger", + "from", + "name", + "fieldId", + "css", + "gutter", + "dataCy", + "tooltipMaxWidth", + "mode", + "aria-labelledby", + "aria-hidden", + "sortKey", + "ouiaId", + "credentialTypeNamespace", + "link", + "value", + "credentialTypeKind", + "linkTo", + "scrollToAlignment", + "displayKey", + "sortedColumnKey", + "maxHeight", + "role", + "aria-haspopup", + "dropDirection", + "resizeOrientation", + "src", + "theme", + "gridColumns" + ], + "ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "START"], + "ignoreComponent": [ + "code", + "Omit", + "PotentialLink", + "TypeRedirect", + "Radio", + "RunOnRadio", + "NodeTypeLetter", + "SelectableItem", + "Dash" + ], + "ignoreCallee": ["describe"] + } + ], "camelcase": "off", "arrow-parens": "off", "comma-dangle": "off", diff --git a/awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx b/awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx index f60323da37..617f7cd51a 100644 --- a/awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx +++ b/awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx @@ -9,7 +9,7 @@ const ActionsGrid = styled.div` align-items: center; ${props => { - const columns = '40px '.repeat(props.numActions || 1); + const columns = props.gridColumns || '40px '.repeat(props.numActions || 1); return css` grid-template-columns: ${columns}; `; @@ -17,7 +17,7 @@ const ActionsGrid = styled.div` `; ActionsGrid.displayName = 'ActionsGrid'; -export default function ActionsTd({ children, ...props }) { +export default function ActionsTd({ children, gridColumns, ...props }) { const numActions = children.length || 1; const width = numActions * 40; return ( @@ -28,7 +28,7 @@ export default function ActionsTd({ children, ...props }) { `} {...props} > - + {React.Children.map(children, (child, i) => React.cloneElement(child, { column: i + 1, diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx index 0bac50fdbc..bc4d1ffd40 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx @@ -6,11 +6,9 @@ import { t } from '@lingui/macro'; import { SchedulesAPI } from '../../../api'; import AlertModal from '../../AlertModal'; import ErrorDetail from '../../ErrorDetail'; +import PaginatedTable, { HeaderRow, HeaderCell } from '../../PaginatedTable'; import DataListToolbar from '../../DataListToolbar'; -import PaginatedDataList, { - ToolbarAddButton, - ToolbarDeleteButton, -} from '../../PaginatedDataList'; +import { ToolbarAddButton, ToolbarDeleteButton } from '../../PaginatedDataList'; import useRequest, { useDeleteItems } from '../../../util/useRequest'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import ScheduleListItem from './ScheduleListItem'; @@ -119,19 +117,27 @@ function ScheduleList({ return ( <> - ( + headerRow={ + + {i18n._(t`Name`)} + {i18n._(t`Type`)} + {i18n._(t`Next Run`)} + + } + renderRow={(item, index) => ( row.id === item.id)} key={item.id} onSelect={() => handleSelect(item)} schedule={item} + rowIndex={index} /> )} toolbarSearchColumns={[ @@ -153,16 +159,6 @@ function ScheduleList({ key: 'modified_by__username__icontains', }, ]} - toolbarSortColumns={[ - { - name: i18n._(t`Name`), - key: 'name', - }, - { - name: i18n._(t`Next Run`), - key: 'next_run', - }, - ]} toolbarSearchableKeys={searchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx index 55440d4a71..963a51cfcf 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx @@ -59,44 +59,61 @@ describe('ScheduleList', () => { test('should check and uncheck the row item', async () => { expect( - wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .props().checked ).toBe(false); await act(async () => { wrapper - .find('DataListCheck[id="select-schedule-1"]') + .find('.pf-c-table__check') + .first() + .find('input') .invoke('onChange')(true); }); wrapper.update(); expect( - wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .props().checked ).toBe(true); await act(async () => { wrapper - .find('DataListCheck[id="select-schedule-1"]') + .find('.pf-c-table__check') + .first() + .find('input') .invoke('onChange')(false); }); wrapper.update(); expect( - wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked + wrapper + .find('.pf-c-table__check') + .first() + .find('input') + .props().checked ).toBe(false); }); test('should check all row items when select all is checked', async () => { - wrapper.find('DataListCheck').forEach(el => { + expect(wrapper.find('.pf-c-table__check input')).toHaveLength(5); + wrapper.find('.pf-c-table__check input').forEach(el => { expect(el.props().checked).toBe(false); }); await act(async () => { wrapper.find('Checkbox#select-all').invoke('onChange')(true); }); wrapper.update(); - wrapper.find('DataListCheck').forEach(el => { + wrapper.find('.pf-c-table__check input').forEach(el => { expect(el.props().checked).toBe(true); }); await act(async () => { wrapper.find('Checkbox#select-all').invoke('onChange')(false); }); wrapper.update(); - wrapper.find('DataListCheck').forEach(el => { + wrapper.find('.pf-c-table__check input').forEach(el => { expect(el.props().checked).toBe(false); }); }); @@ -104,7 +121,8 @@ describe('ScheduleList', () => { test('should call api delete schedules for each selected schedule', async () => { await act(async () => { wrapper - .find('DataListCheck[id="select-schedule-3"]') + .find('.pf-c-table__check input') + .at(3) .invoke('onChange')(); }); wrapper.update(); @@ -122,7 +140,8 @@ describe('ScheduleList', () => { expect(wrapper.find('Modal').length).toBe(0); await act(async () => { wrapper - .find('DataListCheck[id="select-schedule-2"]') + .find('.pf-c-table__check input') + .at(2) .invoke('onChange')(); }); wrapper.update(); diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx index 1bdbe9d4b3..3bfcf9fb55 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx @@ -4,31 +4,16 @@ import { 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 '../../DataListCell'; import { DetailList, Detail } from '../../DetailList'; +import { ActionsTd, ActionItem } from '../../PaginatedTable'; import { ScheduleToggle } from '..'; import { Schedule } from '../../../types'; import { formatDateString } from '../../../util/dates'; -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: 92px 40px; -`; - -function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) { +function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) { const labelId = `check-action-${schedule.id}`; const jobTypeLabels = { @@ -62,69 +47,56 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) { } return ( - - - - - - {schedule.name} - - , - - { - jobTypeLabels[ - schedule.summary_fields.unified_job_template.unified_job_type - ] - } - , - - {schedule.next_run && ( - - - - )} - , - ]} - /> - + + + + {schedule.name} + + + + { + jobTypeLabels[ + schedule.summary_fields.unified_job_template.unified_job_type + ] + } + + + {schedule.next_run && ( + + + + )} + + + + - - {schedule.summary_fields.user_capabilities.edit ? ( - - - - ) : ( - '' - )} - - - + + + + ); } diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx index f9f7ae5d32..2e01a3764e 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx @@ -50,39 +50,47 @@ describe('ScheduleListItem', () => { describe('User has edit permissions', () => { beforeAll(() => { wrapper = mountWithContexts( - + + + + +
); }); + afterAll(() => { wrapper.unmount(); }); + test('Name correctly shown with correct link', () => { expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .text() ).toBe('Mock Schedule'); expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .find('Link') .props().to ).toBe('/templates/job_template/12/schedules/6/details'); }); + test('Type correctly shown', () => { expect( wrapper - .find('DataListCell') - .at(1) + .find('Td') + .at(2) .text() ).toBe('Playbook Run'); }); + test('Edit button shown with correct link', () => { expect(wrapper.find('PencilAltIcon').length).toBe(1); expect( @@ -92,6 +100,7 @@ describe('ScheduleListItem', () => { .props().to ).toBe('/templates/job_template/12/schedules/6/edit'); }); + test('Toggle button enabled', () => { expect( wrapper @@ -100,63 +109,74 @@ describe('ScheduleListItem', () => { .props().isDisabled ).toBe(false); }); - test('Clicking checkbox makes expected callback', () => { + + test('Clicking checkbox selects item', () => { wrapper - .find('DataListCheck') + .find('Td') .first() .find('input') .simulate('change'); expect(onSelect).toHaveBeenCalledTimes(1); }); }); + describe('User has read-only permissions', () => { beforeAll(() => { wrapper = mountWithContexts( - + + + + +
); }); + afterAll(() => { wrapper.unmount(); }); + test('Name correctly shown with correct link', () => { expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .text() ).toBe('Mock Schedule'); expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .find('Link') .props().to ).toBe('/templates/job_template/12/schedules/6/details'); }); + test('Type correctly shown', () => { expect( wrapper - .find('DataListCell') - .at(1) + .find('Td') + .at(2) .text() ).toBe('Playbook Run'); }); + test('Edit button hidden', () => { expect(wrapper.find('PencilAltIcon').length).toBe(0); }); + test('Toggle button disabled', () => { expect( wrapper