From 421d8f215cdec4c5c49e8b2c19c55fb39ff6425b Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Mon, 14 Jun 2021 11:35:38 -0700 Subject: [PATCH] Convert lists to tables - ProjectJobTemplatesList - UserTokenList --- .../ProjectJobTemplatesList.jsx | 63 ++-- .../ProjectJobTemplatesListItem.jsx | 168 ++++----- .../ProjectJobTemplatesListItem.test.jsx | 336 ++++++++++-------- .../User/UserTokenList/UserTokenList.jsx | 36 +- .../User/UserTokenList/UserTokenList.test.jsx | 44 +-- .../User/UserTokenList/UserTokenListItem.jsx | 83 ++--- .../UserTokenList/UserTokenListItem.test.jsx | 117 ++++-- awx/ui_next/src/screens/User/Users.test.jsx | 3 +- 8 files changed, 428 insertions(+), 422 deletions(-) diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx index 7e3bd41738..d08a1b01a2 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx @@ -7,7 +7,11 @@ import { JobTemplatesAPI } from '../../../api'; import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; +import { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; @@ -70,9 +74,13 @@ function ProjectJobTemplatesList() { fetchTemplates(); }, [fetchTemplates]); - const { selected, isAllSelected, handleSelect, setSelected } = useSelected( - jobTemplates - ); + const { + selected, + isAllSelected, + handleSelect, + clearSelected, + selectAll, + } = useSelected(jobTemplates); const { isLoading: isDeleteLoading, @@ -94,7 +102,7 @@ function ProjectJobTemplatesList() { const handleTemplateDelete = async () => { await deleteTemplates(); - setSelected([]); + clearSelected(); }; const canAddJT = @@ -107,14 +115,14 @@ function ProjectJobTemplatesList() { return ( <> - ( @@ -163,9 +145,7 @@ function ProjectJobTemplatesList() { {...props} showSelectAll isAllSelected={isAllSelected} - onSelectAll={isSelected => - setSelected(isSelected ? [...jobTemplates] : []) - } + onSelectAll={selectAll} qsConfig={QS_CONFIG} additionalControls={[ ...(canAddJT ? [addButton] : []), @@ -178,7 +158,15 @@ function ProjectJobTemplatesList() { ]} /> )} - renderItem={template => ( + headerRow={ + + {t`Name`} + {t`Type`} + {t`Recent jobs`} + {t`Actions`} + + } + renderRow={(template, index) => ( handleSelect(template)} isSelected={selected.some(row => row.id === template.id)} + rowIndex={index} /> )} emptyStateControls={canAddJT && addButton} diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx index 388a65cbc7..455d83fedb 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx @@ -1,36 +1,21 @@ import 'styled-components/macro'; import React from 'react'; import { Link } from 'react-router-dom'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; -import { t } from '@lingui/macro'; - +import { Button, Tooltip } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { ExclamationTriangleIcon, PencilAltIcon, RocketIcon, } from '@patternfly/react-icons'; +import { t } from '@lingui/macro'; import styled from 'styled-components'; -import DataListCell from '../../../components/DataListCell'; +import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { LaunchButton } from '../../../components/LaunchButton'; import Sparkline from '../../../components/Sparkline'; import { toTitleCase } from '../../../util/strings'; -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: repeat(2, 40px); -`; - const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)` color: var(--pf-global--warning-color--100); margin-left: 18px; @@ -41,8 +26,8 @@ function ProjectJobTemplateListItem({ isSelected, onSelect, detailUrl, + rowIndex, }) { - const labelId = `check-action-${template.id}`; const canLaunch = template.summary_fields.user_capabilities.start; const missingResourceIcon = @@ -57,90 +42,75 @@ function ProjectJobTemplateListItem({ !template.execution_environment; return ( - - - - - - - {template.name} - - - {missingResourceIcon && ( - - - - - - )} - {missingExecutionEnvironment && ( - - - - - - )} - , - - {toTitleCase(template.type)} - , - - - , - ]} - /> - + + + + {template.name} + {missingResourceIcon && ( + + + + )} + {missingExecutionEnvironment && ( + + + + )} + + + {toTitleCase(template.type)} + + + + + - {canLaunch && template.type === 'job_template' && ( - - - {({ handleLaunch, isLaunching }) => ( - - )} - - - )} - {template.summary_fields.user_capabilities.edit && ( - + + {({ handleLaunch, isLaunching }) => ( - - )} - - - + )} + + + + + + + ); } diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx index 895d76a807..d61ed8f720 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx @@ -7,157 +7,195 @@ import ProjectJobTemplatesListItem from './ProjectJobTemplatesListItem'; describe('', () => { test('launch button shown to users with start capabilities', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('LaunchButton').exists()).toBeTruthy(); }); + test('launch button hidden from users without start capabilities', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('LaunchButton').exists()).toBeFalsy(); }); + test('edit button shown to users with edit capabilities', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); + test('edit button hidden from users without edit capabilities', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); }); + test('missing resource icon is shown.', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(true); }); + test('missing resource icon is not shown when there is a project and an inventory.', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); }); + test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); }); test('missing resource icon is not shown type is workflow_job_template', () => { const wrapper = mountWithContexts( - + + + + +
); expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); }); @@ -166,19 +204,23 @@ describe('', () => { initialEntries: ['/projects/1/job_templates'], }); const wrapper = mountWithContexts( - , + + + + +
, { context: { router: { history } } } ); wrapper.find('Link').simulate('click', { button: 0 }); @@ -189,22 +231,26 @@ describe('', () => { test('should render warning about missing execution environment', () => { const wrapper = mountWithContexts( - + + + + +
); expect( diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx index 2c111d9be3..034d52d4cb 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx @@ -3,7 +3,11 @@ import { useLocation, useParams } from 'react-router-dom'; import { t } from '@lingui/macro'; import { getQSConfig, parseQueryString } from '../../../util/qs'; -import PaginatedDataList, { +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; +import { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; @@ -68,9 +72,13 @@ function UserTokenList() { fetchTokens(); }, [fetchTokens]); - const { selected, isAllSelected, handleSelect, setSelected } = useSelected( - tokens - ); + const { + selected, + isAllSelected, + handleSelect, + clearSelected, + selectAll, + } = useSelected(tokens); const { isLoading: isDeleteLoading, @@ -91,21 +99,21 @@ function UserTokenList() { ); const handleDelete = async () => { await deleteTokens(); - setSelected([]); + clearSelected(); }; const canAdd = true; return ( <> - - setSelected(isSelected ? [...tokens] : []) - } + onSelectAll={selectAll} additionalControls={[ ...(canAdd ? [ @@ -168,7 +174,14 @@ function UserTokenList() { ]} /> )} - renderItem={token => ( + headerRow={ + + {t`Name`} + {t`Scope`} + {t`Expires`} + + } + renderRow={(token, index) => ( row.id === token.id)} + rowIndex={index} /> )} emptyStateControls={ diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx index 2b05d2ef35..983df13629 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx @@ -143,32 +143,6 @@ describe('', () => { expect(wrapper.find('UserTokenList').length).toBe(1); }); - test('edit button should be disabled', async () => { - await act(async () => { - wrapper = mountWithContexts(); - }); - waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); - }); - - test('should enable edit button', async () => { - UsersAPI.readTokens.mockResolvedValue(tokens); - await act(async () => { - wrapper = mountWithContexts(); - }); - waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); - expect( - wrapper.find('DataListCheck[id="select-token-3"]').props().checked - ).toBe(false); - await act(async () => { - wrapper.find('DataListCheck[id="select-token-3"]').invoke('onChange')( - true - ); - }); - wrapper.update(); - expect( - wrapper.find('DataListCheck[id="select-token-3"]').props().checked - ).toBe(true); - }); test('delete button should be disabled', async () => { UsersAPI.readTokens.mockResolvedValue(tokens); await act(async () => { @@ -179,6 +153,7 @@ describe('', () => { true ); }); + test('should select and then delete item properly', async () => { UsersAPI.readTokens.mockResolvedValue(tokens); await act(async () => { @@ -190,13 +165,17 @@ describe('', () => { ); await act(async () => { wrapper - .find('DataListCheck[aria-labelledby="check-action-3"]') + .find('.pf-c-table__check') + .at(2) + .find('input') .prop('onChange')(tokens.data.results[0]); }); wrapper.update(); expect( wrapper - .find('DataListCheck[aria-labelledby="check-action-3"]') + .find('.pf-c-table__check') + .at(2) + .find('input') .prop('checked') ).toBe(true); expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( @@ -213,6 +192,7 @@ describe('', () => { wrapper.update(); expect(TokensAPI.destroy).toHaveBeenCalledWith(3); }); + test('should select and then delete item properly', async () => { UsersAPI.readTokens.mockResolvedValue(tokens); TokensAPI.destroy.mockRejectedValue( @@ -236,13 +216,17 @@ describe('', () => { ); await act(async () => { wrapper - .find('DataListCheck[aria-labelledby="check-action-3"]') + .find('.pf-c-table__check') + .at(2) + .find('input') .prop('onChange')(tokens.data.results[0]); }); wrapper.update(); expect( wrapper - .find('DataListCheck[aria-labelledby="check-action-3"]') + .find('.pf-c-table__check') + .at(2) + .find('input') .prop('checked') ).toBe(true); expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx index 5248802e1e..df246aa5d9 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx @@ -2,74 +2,31 @@ import React from 'react'; import { Link, useParams } from 'react-router-dom'; import { t } from '@lingui/macro'; -import { - DataListItemCells, - DataListCheck, - DataListItemRow, - DataListItem, -} from '@patternfly/react-core'; -import styled from 'styled-components'; +import { Tr, Td } from '@patternfly/react-table'; import { toTitleCase } from '../../../util/strings'; - import { formatDateString } from '../../../util/dates'; -import DataListCell from '../../../components/DataListCell'; -const Label = styled.b` - margin-right: 20px; -`; - -const NameLabel = styled.b` - margin-right: 5px; -`; - -function UserTokenListItem({ token, isSelected, onSelect }) { +function UserTokenListItem({ token, isSelected, onSelect, rowIndex }) { const { id } = useParams(); - const labelId = `check-action-${token.id}`; return ( - - - - - - {token.summary_fields?.application - ? t`Application access token` - : t`Personal access token`} - - , - - {token.summary_fields?.application && ( - - {t`Application`} - - {token.summary_fields.application.name} - - - )} - , - - - {toTitleCase(token.scope)} - , - - - {formatDateString(token.expires)} - , - ]} - /> - - + + + + + {token.summary_fields?.application + ? token.summary_fields.application.name + : `Personal access token`} + + + {toTitleCase(token.scope)} + {formatDateString(token.expires)} + ); } diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx index 87bc06401c..9d36114e90 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx @@ -39,7 +39,13 @@ describe('', () => { let wrapper; test('should mount properly', async () => { await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + + + +
+ ); }); expect(wrapper.find('UserTokenListItem').length).toBe(1); }); @@ -47,62 +53,101 @@ describe('', () => { test('should render application access token row properly', async () => { await act(async () => { wrapper = mountWithContexts( - + + + + +
); }); - expect(wrapper.find('DataListCheck').prop('checked')).toBe(false); - expect(wrapper.find('PFDataListCell[aria-label="Token type"]').text()).toBe( - 'Application access token' - ); expect( - wrapper.find('PFDataListCell[aria-label="Application name"]').text() - ).toContain('Foobar app'); - expect(wrapper.find('PFDataListCell[aria-label="Scope"]').text()).toContain( - 'Read' - ); + wrapper + .find('Td') + .first() + .prop('select').isSelected + ).toBe(false); expect( - wrapper.find('PFDataListCell[aria-label="Expiration"]').text() + wrapper + .find('Td') + .at(1) + .text() + ).toBe('Foobar app'); + expect( + wrapper + .find('Td') + .at(2) + .text() + ).toContain('Read'); + expect( + wrapper + .find('Td') + .at(3) + .text() ).toContain('10/25/3019, 3:06:43 PM'); }); test('should render personal access token row properly', async () => { await act(async () => { wrapper = mountWithContexts( - + + + + +
); }); - expect(wrapper.find('DataListCheck').prop('checked')).toBe(false); - expect(wrapper.find('PFDataListCell[aria-label="Token type"]').text()).toBe( - 'Personal access token' - ); expect( - wrapper.find('PFDataListCell[aria-label="Application name"]').text() - ).toBe(''); - expect(wrapper.find('PFDataListCell[aria-label="Scope"]').text()).toContain( - 'Write' - ); + wrapper + .find('Td') + .first() + .prop('select').isSelected + ).toBe(false); expect( - wrapper.find('PFDataListCell[aria-label="Expiration"]').text() + wrapper + .find('Td') + .at(1) + .text() + ).toEqual('Personal access token'); + expect( + wrapper + .find('Td') + .at(2) + .text() + ).toEqual('Write'); + expect( + wrapper + .find('Td') + .at(3) + .text() ).toContain('10/25/3019, 3:06:43 PM'); }); test('should be checked', async () => { await act(async () => { wrapper = mountWithContexts( - + + + + +
); }); - expect(wrapper.find('DataListCheck').prop('checked')).toBe(true); + expect( + wrapper + .find('Td') + .first() + .prop('select').isSelected + ).toBe(true); }); }); diff --git a/awx/ui_next/src/screens/User/Users.test.jsx b/awx/ui_next/src/screens/User/Users.test.jsx index 862934e99b..146599b762 100644 --- a/awx/ui_next/src/screens/User/Users.test.jsx +++ b/awx/ui_next/src/screens/User/Users.test.jsx @@ -11,7 +11,8 @@ jest.mock('react-router-dom', () => ({ describe('', () => { test('initially renders successfully', () => { - mountWithContexts(); + const wrapper = mountWithContexts(); + wrapper.unmount(); }); test('should display a breadcrumb heading', () => {