From cfc4c3a1a79b23578a89a0acbd00724a21c9c989 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Nov 2020 16:29:57 -0800 Subject: [PATCH] add patternfly tables; add PaginatedTable --- awx/ui_next/package-lock.json | 52 ++++ awx/ui_next/package.json | 1 + .../components/PaginatedTable/ActionsTd.jsx | 33 +++ .../PaginatedTable/PaginatedTable.jsx | 227 ++++++++++++++++++ .../PaginatedTable/PaginatedTableRow.jsx | 42 ++++ .../src/components/PaginatedTable/index.js | 5 + .../OrganizationList/OrganizationList.jsx | 30 ++- .../OrganizationList/OrganizationListItem.jsx | 122 +++------- 8 files changed, 421 insertions(+), 91 deletions(-) create mode 100644 awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx create mode 100644 awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx create mode 100644 awx/ui_next/src/components/PaginatedTable/PaginatedTableRow.jsx create mode 100644 awx/ui_next/src/components/PaginatedTable/index.js diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index 8cd533bb1f..c0f092a399 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -3194,6 +3194,58 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.7.22.tgz", "integrity": "sha512-ojNuSNJx6CkNtsSFseZ2SJEVyzPMFYh0jOs204ICzYM1+fn9acsIi3Co0bcskFRzw8F6e2/x+8uVNx6QI8elxg==" }, + "@patternfly/react-table": { + "version": "4.19.15", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.19.15.tgz", + "integrity": "sha512-WeeiOlJs96QfT7/TChpaCUAsl0hsWsLelJIR68b0LA+gzTEJzaXn0m5NL9ht4LL3edT4hwMz0jVz98xkStmvBg==", + "requires": { + "@patternfly/patternfly": "4.59.1", + "@patternfly/react-core": "^4.75.10", + "@patternfly/react-icons": "^4.7.16", + "@patternfly/react-styles": "^4.7.13", + "@patternfly/react-tokens": "^4.9.16", + "lodash": "^4.17.19", + "tslib": "1.13.0" + }, + "dependencies": { + "@patternfly/react-core": { + "version": "4.79.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.79.2.tgz", + "integrity": "sha512-TCWi5Hu8+gpqFVAL4ZMXCRLbRfayM7wJ8+/Ob4rfhC61qm36CZNAcqWOmuV8bghOzB29INUMNShggtuiUa5mkg==", + "requires": { + "@patternfly/react-icons": "^4.7.18", + "@patternfly/react-styles": "^4.7.16", + "@patternfly/react-tokens": "^4.9.18", + "focus-trap": "4.0.2", + "react-dropzone": "9.0.0", + "tippy.js": "5.1.2", + "tslib": "1.13.0" + }, + "dependencies": { + "@patternfly/react-icons": { + "version": "4.7.18", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.7.18.tgz", + "integrity": "sha512-Kd0JjeVCESpMJGb5ZkLXvAdCuklV9dYGUkcTO18WMyXQ57s9+xXjVA77wojmp6Ru1ZCWOP5bLXZOKmwVnOfUpQ==" + }, + "@patternfly/react-tokens": { + "version": "4.9.18", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.9.18.tgz", + "integrity": "sha512-zQfqwKtoz1hDngyiGnF6oHeESDtgNY6C79Db97JxMMuRBV7i+5f6uC/DrYhcqNtqHA7mxrVJg0SM1xnPSAW9lA==" + } + } + }, + "@patternfly/react-styles": { + "version": "4.7.16", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.7.16.tgz", + "integrity": "sha512-bJmRrYKXgHGPPwLHg/gy1tDb/qEV6JpFLgkelLuz38czXeBnPpAUn9yKry3wNr95VQGERT6FcLsWjXKPY1x42Q==" + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, "@patternfly/react-tokens": { "version": "4.9.22", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.9.22.tgz", diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index e28ff7fb92..551f0cb543 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -10,6 +10,7 @@ "@patternfly/patternfly": "4.70.2", "@patternfly/react-core": "4.84.3", "@patternfly/react-icons": "4.7.22", + "@patternfly/react-table": "^4.19.15", "ansi-to-html": "^0.6.11", "axios": "^0.18.1", "codemirror": "^5.47.0", diff --git a/awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx b/awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx new file mode 100644 index 0000000000..53c6c1cb19 --- /dev/null +++ b/awx/ui_next/src/components/PaginatedTable/ActionsTd.jsx @@ -0,0 +1,33 @@ +import 'styled-components/macro'; +import React from 'react'; +import { Td } from '@patternfly/react-table'; +import styled, { css } from 'styled-components'; + +// table cells will automatically grow beyond specified width to accomodate +// multiple action buttons +const ActionsGrid = styled.div` + display: grid; + grid-gap: 16px; + align-items: center; + + ${props => { + const columns = '40px '.repeat(props.numActions || 1); + return css` + grid-template-columns: ${columns}; + `; + }} +`; + +export default function ActionsTd({ numActions = 1, children }) { + const width = numActions * 40; + return ( + + {children} + + ); +} diff --git a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx new file mode 100644 index 0000000000..d797fde02b --- /dev/null +++ b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx @@ -0,0 +1,227 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { TableComposable, Tbody } from '@patternfly/react-table'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { withRouter } from 'react-router-dom'; + +import ListHeader from '../ListHeader'; +import ContentEmpty from '../ContentEmpty'; +import ContentError from '../ContentError'; +import ContentLoading from '../ContentLoading'; +import Pagination from '../Pagination'; +import DataListToolbar from '../DataListToolbar'; + +import { + encodeNonDefaultQueryString, + parseQueryString, + replaceParams, +} from '../../util/qs'; + +import { QSConfig, SearchColumns, SortColumns } from '../../types'; + +import PaginatedTableRow from './PaginatedTableRow'; + +class PaginatedTable extends React.Component { + constructor(props) { + super(props); + this.handleSetPage = this.handleSetPage.bind(this); + this.handleSetPageSize = this.handleSetPageSize.bind(this); + this.handleListItemSelect = this.handleListItemSelect.bind(this); + } + + handleListItemSelect = (id = 0) => { + const { items, onRowClick } = this.props; + const match = items.find(item => item.id === Number(id)); + onRowClick(match); + }; + + handleSetPage(event, pageNumber) { + const { history, qsConfig } = this.props; + const { search } = history.location; + const oldParams = parseQueryString(qsConfig, search); + this.pushHistoryState(replaceParams(oldParams, { page: pageNumber })); + } + + handleSetPageSize(event, pageSize, page) { + const { history, qsConfig } = this.props; + const { search } = history.location; + const oldParams = parseQueryString(qsConfig, search); + this.pushHistoryState( + replaceParams(oldParams, { page_size: pageSize, page }) + ); + } + + pushHistoryState(params) { + const { history, qsConfig } = this.props; + const { pathname } = history.location; + const encodedParams = encodeNonDefaultQueryString(qsConfig, params); + history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); + } + + render() { + const { + contentError, + hasContentLoading, + emptyStateControls, + items, + itemCount, + qsConfig, + headerRow, + renderRow, + toolbarSearchColumns, + toolbarSearchableKeys, + toolbarRelatedSearchableKeys, + toolbarSortColumns, + pluralizedItemName, + showPageSizeOptions, + location, + i18n, + renderToolbar, + } = this.props; + const searchColumns = toolbarSearchColumns.length + ? toolbarSearchColumns + : [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + ]; + const sortColumns = toolbarSortColumns.length + ? toolbarSortColumns + : [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ]; + const queryParams = parseQueryString(qsConfig, location.search); + + const dataListLabel = i18n._(t`${pluralizedItemName} List`); + const emptyContentMessage = i18n._( + t`Please add ${pluralizedItemName} to populate this list ` + ); + const emptyContentTitle = i18n._(t`No ${pluralizedItemName} Found `); + + let Content; + if (hasContentLoading && items.length <= 0) { + Content = ; + } else if (contentError) { + Content = ; + } else if (items.length <= 0) { + Content = ( + + ); + } else { + Content = ( + this.handleListItemSelect(id)} + > + {headerRow} + {items.map(renderRow)} + + ); + } + + const ToolbarPagination = ( + + ); + + return ( + + + {Content} + {items.length ? ( + + ) : null} + + ); + } +} + +const Item = PropTypes.shape({ + id: PropTypes.number.isRequired, + url: PropTypes.string.isRequired, + name: PropTypes.string, +}); + +PaginatedTable.propTypes = { + items: PropTypes.arrayOf(Item).isRequired, + itemCount: PropTypes.number.isRequired, + pluralizedItemName: PropTypes.string, + qsConfig: QSConfig.isRequired, + renderRow: PropTypes.func, + toolbarSearchColumns: SearchColumns, + toolbarSearchableKeys: PropTypes.arrayOf(PropTypes.string), + toolbarRelatedSearchableKeys: PropTypes.arrayOf(PropTypes.string), + toolbarSortColumns: SortColumns, + showPageSizeOptions: PropTypes.bool, + renderToolbar: PropTypes.func, + hasContentLoading: PropTypes.bool, + contentError: PropTypes.shape(), + onRowClick: PropTypes.func, +}; + +PaginatedTable.defaultProps = { + hasContentLoading: false, + contentError: null, + toolbarSearchColumns: [], + toolbarSearchableKeys: [], + toolbarRelatedSearchableKeys: [], + toolbarSortColumns: [], + pluralizedItemName: 'Items', + showPageSizeOptions: true, + renderRow: item => , + renderToolbar: props => , + onRowClick: () => null, +}; + +export { PaginatedTable as _PaginatedTable }; +export default withI18n()(withRouter(PaginatedTable)); diff --git a/awx/ui_next/src/components/PaginatedTable/PaginatedTableRow.jsx b/awx/ui_next/src/components/PaginatedTable/PaginatedTableRow.jsx new file mode 100644 index 0000000000..6db53493cb --- /dev/null +++ b/awx/ui_next/src/components/PaginatedTable/PaginatedTableRow.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + DataListItem, + DataListItemRow, + DataListItemCells, + TextContent, +} from '@patternfly/react-core'; +import styled from 'styled-components'; +import DataListCell from '../DataListCell'; + +const DetailWrapper = styled(TextContent)` + display: grid; + grid-template-columns: + minmax(70px, max-content) + repeat(auto-fit, minmax(60px, max-content)); + grid-gap: 10px; +`; + +export default function PaginatedDataListItem({ item }) { + return ( + + + + + + {item.name} + + + , + ]} + /> + + + ); +} diff --git a/awx/ui_next/src/components/PaginatedTable/index.js b/awx/ui_next/src/components/PaginatedTable/index.js new file mode 100644 index 0000000000..3604417d95 --- /dev/null +++ b/awx/ui_next/src/components/PaginatedTable/index.js @@ -0,0 +1,5 @@ +export { default } from './PaginatedTable'; +export { default as PaginatedTableRow } from './PaginatedTableRow'; +export { default as ActionsTd } from './ActionsTd'; +// export { default as ToolbarDeleteButton } from './ToolbarDeleteButton'; +// export { default as ToolbarAddButton } from './ToolbarAddButton'; diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx index 9b4dca7413..6ddfe41470 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx @@ -3,16 +3,18 @@ import { useLocation, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Card, PageSection } from '@patternfly/react-core'; +import { Thead, Tr, Th } from '@patternfly/react-table'; import { OrganizationsAPI } from '../../../api'; 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 from '../../../components/PaginatedTable'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import OrganizationListItem from './OrganizationListItem'; @@ -117,7 +119,7 @@ function OrganizationsList({ i18n }) { <> - + + + {i18n._(t`Name`)} + {i18n._(t`Members`)} + {i18n._(t`Teams`)} + + + } renderToolbar={props => ( )} - renderItem={o => ( + renderRow={(o, index) => ( row.id === o.id)} onSelect={() => handleSelect(o)} diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx index 37d01a9e0a..c251bdebc5 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx @@ -2,105 +2,57 @@ import React from 'react'; import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Badge as PFBadge, - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemCells, - DataListItemRow, - Tooltip, -} from '@patternfly/react-core'; - +import { Button, Tooltip } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { Link } from 'react-router-dom'; -import styled from 'styled-components'; import { PencilAltIcon } from '@patternfly/react-icons'; -import DataListCell from '../../../components/DataListCell'; +import { ActionsTd } from '../../../components/PaginatedTable'; import { Organization } from '../../../types'; -const Badge = styled(PFBadge)` - margin-left: 8px; -`; - -const ListGroup = styled.span` - margin-left: 24px; - - &:first-of-type { - margin-left: 0; - } -`; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: 40px; -`; - function OrganizationListItem({ organization, isSelected, onSelect, + rowIndex, detailUrl, i18n, }) { const labelId = `check-action-${organization.id}`; return ( - - - - - - {organization.name} - - , - - - {i18n._(t`Members`)} - - {organization.summary_fields.related_field_counts.users} - - - - {i18n._(t`Teams`)} - - {organization.summary_fields.related_field_counts.teams} - - - , - ]} - /> - - {organization.summary_fields.user_capabilities.edit ? ( - - - - ) : ( - '' - )} - - - + + + + + {organization.name} + + + {organization.summary_fields.related_field_counts.users} + {organization.summary_fields.related_field_counts.teams} + + {organization.summary_fields.user_capabilities.edit ? ( + + + + ) : ( + '' + )} + + ); }