add patternfly tables; add PaginatedTable

This commit is contained in:
Keith Grant
2020-11-19 16:29:57 -08:00
parent e08e88d940
commit cfc4c3a1a7
8 changed files with 421 additions and 91 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
<Td
css={`
text-align: right;
--pf-c-table--cell--Width: ${width}px;
`}
>
<ActionsGrid numActions={numActions}>{children}</ActionsGrid>
</Td>
);
}

View File

@@ -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 = <ContentLoading />;
} else if (contentError) {
Content = <ContentError error={contentError} />;
} else if (items.length <= 0) {
Content = (
<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />
);
} else {
Content = (
<TableComposable
aria-label={dataListLabel}
onSelectDataListItem={id => this.handleListItemSelect(id)}
>
{headerRow}
<Tbody>{items.map(renderRow)}</Tbody>
</TableComposable>
);
}
const ToolbarPagination = (
<Pagination
isCompact
dropDirection="down"
itemCount={itemCount}
page={queryParams.page || 1}
perPage={queryParams.page_size}
perPageOptions={
showPageSizeOptions
? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 },
]
: []
}
onSetPage={this.handleSetPage}
onPerPageSelect={this.handleSetPageSize}
/>
);
return (
<Fragment>
<ListHeader
itemCount={itemCount}
renderToolbar={renderToolbar}
emptyStateControls={emptyStateControls}
searchColumns={searchColumns}
sortColumns={sortColumns}
searchableKeys={toolbarSearchableKeys}
relatedSearchableKeys={toolbarRelatedSearchableKeys}
qsConfig={qsConfig}
pagination={ToolbarPagination}
/>
{Content}
{items.length ? (
<Pagination
variant="bottom"
itemCount={itemCount}
page={queryParams.page || 1}
perPage={queryParams.page_size}
perPageOptions={
showPageSizeOptions
? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 },
]
: []
}
onSetPage={this.handleSetPage}
onPerPageSelect={this.handleSetPageSize}
/>
) : null}
</Fragment>
);
}
}
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 => <PaginatedTableRow key={item.id} item={item} />,
renderToolbar: props => <DataListToolbar {...props} />,
onRowClick: () => null,
};
export { PaginatedTable as _PaginatedTable };
export default withI18n()(withRouter(PaginatedTable));

View File

@@ -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 (
<DataListItem
aria-labelledby={`items-list-item-${item.id}`}
key={item.id}
id={`${item.id}`}
>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key="name">
<DetailWrapper>
<Link to={{ pathname: item.url }}>
<b id={`items-list-item-${item.id}`}>{item.name}</b>
</Link>
</DetailWrapper>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
);
}

View File

@@ -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';

View File

@@ -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 }) {
<>
<PageSection>
<Card>
<PaginatedDataList
<PaginatedTable
contentError={contentError}
hasContentLoading={hasContentLoading}
items={organizations}
@@ -152,12 +154,27 @@ function OrganizationsList({ i18n }) {
]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={
<Thead>
<Tr>
<Th
select={{
onSelect: handleSelectAll,
isSelected: isAllSelected,
}}
/>
<Th>{i18n._(t`Name`)}</Th>
<Th>{i18n._(t`Members`)}</Th>
<Th>{i18n._(t`Teams`)}</Th>
</Tr>
</Thead>
}
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={handleSelectAll}
// showSelectAll
// isAllSelected={isAllSelected}
// onSelectAll={handleSelectAll}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
@@ -172,10 +189,11 @@ function OrganizationsList({ i18n }) {
]}
/>
)}
renderItem={o => (
renderRow={(o, index) => (
<OrganizationListItem
key={o.id}
organization={o}
rowIndex={index}
detailUrl={`${match.url}/${o.id}`}
isSelected={selected.some(row => row.id === o.id)}
onSelect={() => handleSelect(o)}

View File

@@ -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 (
<DataListItem
key={organization.id}
aria-labelledby={labelId}
id={`${organization.id}`}
>
<DataListItemRow>
<DataListCheck
id={`select-organization-${organization.id}`}
checked={isSelected}
onChange={onSelect}
aria-labelledby={labelId}
/>
<DataListItemCells
dataListCells={[
<DataListCell key="name" id={labelId}>
<Link to={`${detailUrl}`}>
<b>{organization.name}</b>
</Link>
</DataListCell>,
<DataListCell key="related-field-counts">
<ListGroup>
{i18n._(t`Members`)}
<Badge isRead>
{organization.summary_fields.related_field_counts.users}
</Badge>
</ListGroup>
<ListGroup>
{i18n._(t`Teams`)}
<Badge isRead>
{organization.summary_fields.related_field_counts.teams}
</Badge>
</ListGroup>
</DataListCell>,
]}
/>
<DataListAction aria-label="actions" aria-labelledby={labelId}>
{organization.summary_fields.user_capabilities.edit ? (
<Tooltip content={i18n._(t`Edit Organization`)} position="top">
<Button
aria-label={i18n._(t`Edit Organization`)}
variant="plain"
component={Link}
to={`/organizations/${organization.id}/edit`}
>
<PencilAltIcon />
</Button>
</Tooltip>
) : (
''
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
<Tr id={`${organization.id}`}>
<Td
select={{
rowIndex,
isSelected,
onSelect,
disable: false,
}}
/>
<Td id={labelId}>
<Link to={`${detailUrl}`}>
<b>{organization.name}</b>
</Link>
</Td>
<Td>{organization.summary_fields.related_field_counts.users}</Td>
<Td>{organization.summary_fields.related_field_counts.teams}</Td>
<ActionsTd numActions={2}>
{organization.summary_fields.user_capabilities.edit ? (
<Tooltip content={i18n._(t`Edit Organization`)} position="top">
<Button
aria-label={i18n._(t`Edit Organization`)}
variant="plain"
component={Link}
to={`/organizations/${organization.id}/edit`}
>
<PencilAltIcon />
</Button>
</Tooltip>
) : (
''
)}
</ActionsTd>
</Tr>
);
}