convert inventory access/hosts lists to tables

This commit is contained in:
Keith J. Grant
2021-04-23 11:00:16 -07:00
parent f8ecdbf287
commit 0ac6ba9c99
5 changed files with 117 additions and 194 deletions

View File

@@ -47,7 +47,11 @@ function HostToggle({
return ( return (
<Fragment> <Fragment>
<Tooltip content={tooltip} position="top"> <Tooltip
content={tooltip}
position="top"
css="white-space: nowrap; --pf-c-table--cell--WhiteSpace: nowrap"
>
<Switch <Switch
className={className} className={className}
css="display: inline-flex;" css="display: inline-flex;"

View File

@@ -6,7 +6,8 @@ import { RolesAPI, TeamsAPI, UsersAPI } from '../../api';
import AddResourceRole from '../AddRole/AddResourceRole'; import AddResourceRole from '../AddRole/AddResourceRole';
import AlertModal from '../AlertModal'; import AlertModal from '../AlertModal';
import DataListToolbar from '../DataListToolbar'; import DataListToolbar from '../DataListToolbar';
import PaginatedDataList, { ToolbarAddButton } from '../PaginatedDataList'; import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable';
import { ToolbarAddButton } from '../PaginatedDataList';
import { getQSConfig, parseQueryString } from '../../util/qs'; import { getQSConfig, parseQueryString } from '../../util/qs';
import useRequest, { useDeleteItems } from '../../util/useRequest'; import useRequest, { useDeleteItems } from '../../util/useRequest';
import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal'; import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal';
@@ -148,7 +149,7 @@ function ResourceAccessList({ apiModel, resource }) {
return ( return (
<> <>
<PaginatedDataList <PaginatedTable
error={contentError} error={contentError}
hasContentLoading={isLoading || isDeleteLoading} hasContentLoading={isLoading || isDeleteLoading}
items={accessRecords} items={accessRecords}
@@ -156,20 +157,6 @@ function ResourceAccessList({ apiModel, resource }) {
pluralizedItemName={t`Roles`} pluralizedItemName={t`Roles`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarSearchColumns={toolbarSearchColumns} toolbarSearchColumns={toolbarSearchColumns}
toolbarSortColumns={[
{
name: t`Username`,
key: 'username',
},
{
name: t`First Name`,
key: 'first_name',
},
{
name: t`Last Name`,
key: 'last_name',
},
]}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderToolbar={props => ( renderToolbar={props => (
@@ -188,7 +175,15 @@ function ResourceAccessList({ apiModel, resource }) {
} }
/> />
)} )}
renderItem={accessRecord => ( headerRow={
<HeaderRow qsConfig={QS_CONFIG} isSelectable={false}>
<HeaderCell sortKey="username">{t`Username`}</HeaderCell>
<HeaderCell sortKey="first_name">{t`First name`}</HeaderCell>
<HeaderCell sortKey="last_name">{t`Last name`}</HeaderCell>
<HeaderCell>{t`Roles`}</HeaderCell>
</HeaderRow>
}
renderRow={(accessRecord, index) => (
<ResourceAccessListItem <ResourceAccessListItem
key={accessRecord.id} key={accessRecord.id}
accessRecord={accessRecord} accessRecord={accessRecord}
@@ -197,6 +192,7 @@ function ResourceAccessList({ apiModel, resource }) {
setDeletionRole(role); setDeletionRole(role);
setShowDeleteModal(true); setShowDeleteModal(true);
}} }}
rowIndex={index}
/> />
)} )}
/> />

View File

@@ -3,27 +3,14 @@ import React from 'react';
import { func } from 'prop-types'; import { func } from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Chip } from '@patternfly/react-core';
Chip, import { Tr, Td } from '@patternfly/react-table';
DataListItem,
DataListItemRow,
DataListItemCells as PFDataListItemCells,
Text,
TextContent,
TextVariants,
} from '@patternfly/react-core';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styled from 'styled-components';
import DataListCell from '../DataListCell';
import ChipGroup from '../ChipGroup'; import ChipGroup from '../ChipGroup';
import { DetailList, Detail } from '../DetailList'; import { DetailList, Detail } from '../DetailList';
import { AccessRecord } from '../../types'; import { AccessRecord } from '../../types';
const DataListItemCells = styled(PFDataListItemCells)`
align-items: start;
`;
function ResourceAccessListItem({ accessRecord, onRoleDelete }) { function ResourceAccessListItem({ accessRecord, onRoleDelete }) {
ResourceAccessListItem.propTypes = { ResourceAccessListItem.propTypes = {
accessRecord: AccessRecord.isRequired, accessRecord: AccessRecord.isRequired,
@@ -67,70 +54,43 @@ function ResourceAccessListItem({ accessRecord, onRoleDelete }) {
const [teamRoles, userRoles] = getRoleLists(); const [teamRoles, userRoles] = getRoleLists();
return ( return (
<DataListItem <Tr id={`access-item-row-${accessRecord.id}`}>
aria-labelledby="access-list-item" <Td id={`access-record-${accessRecord.id}`} dataLabel={t`Name`}>
key={accessRecord.id} {accessRecord.id ? (
id={`${accessRecord.id}`} <Link to={{ pathname: `/users/${accessRecord.id}/details` }}>
> {accessRecord.username}
<DataListItemRow> </Link>
<DataListItemCells ) : (
dataListCells={[ accessRecord.username
<DataListCell key="name"> )}
{accessRecord.username && ( </Td>
<TextContent> <Td dataLabel={t`First name`}>{accessRecord.first_name}</Td>
{accessRecord.id ? ( <Td dataLabel={t`Last name`}>{accessRecord.first_name}</Td>
<Text component={TextVariants.h6}> <Td dataLabel={t`Roles`}>
<Link <DetailList stacked>
to={{ pathname: `/users/${accessRecord.id}/details` }} {userRoles.length > 0 && (
css="font-weight: bold" <Detail
> label={t`User Roles`}
{accessRecord.username} value={
</Link> <ChipGroup numChips={5} totalChips={userRoles.length}>
</Text> {userRoles.map(renderChip)}
) : ( </ChipGroup>
<Text component={TextVariants.h6} css="font-weight: bold"> }
{accessRecord.username} />
</Text> )}
)} {teamRoles.length > 0 && (
</TextContent> <Detail
)} label={t`Team Roles`}
{accessRecord.first_name || accessRecord.last_name ? ( value={
<DetailList stacked> <ChipGroup numChips={5} totalChips={teamRoles.length}>
<Detail {teamRoles.map(renderChip)}
label={t`Name`} </ChipGroup>
value={`${accessRecord.first_name} ${accessRecord.last_name}`} }
/> />
</DetailList> )}
) : null} </DetailList>
</DataListCell>, </Td>
<DataListCell key="roles"> </Tr>
<DetailList stacked>
{userRoles.length > 0 && (
<Detail
label={t`User Roles`}
value={
<ChipGroup numChips={5} totalChips={userRoles.length}>
{userRoles.map(renderChip)}
</ChipGroup>
}
/>
)}
{teamRoles.length > 0 && (
<Detail
label={t`Team Roles`}
value={
<ChipGroup numChips={5} totalChips={teamRoles.length}>
{teamRoles.map(renderChip)}
</ChipGroup>
}
/>
)}
</DetailList>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -2,83 +2,55 @@ import React from 'react';
import { string, bool, func } from 'prop-types'; import { string, bool, func } from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Button, Tooltip } from '@patternfly/react-core';
Button, import { Tr, Td } from '@patternfly/react-table';
DataListAction as _DataListAction,
DataListCheck,
DataListItem,
DataListItemCells,
DataListItemRow,
Tooltip,
} from '@patternfly/react-core';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { PencilAltIcon } from '@patternfly/react-icons'; import { PencilAltIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import { ActionsTd } from '../../../components/PaginatedTable';
import DataListCell from '../../../components/DataListCell';
import HostToggle from '../../../components/HostToggle'; import HostToggle from '../../../components/HostToggle';
import Sparkline from '../../../components/Sparkline';
import { Host } from '../../../types'; import { Host } from '../../../types';
const DataListAction = styled(_DataListAction)` function InventoryHostItem({
align-items: center; detailUrl,
display: grid; editUrl,
grid-gap: 24px; host,
grid-template-columns: min-content 40px; isSelected,
`; onSelect,
rowIndex,
function InventoryHostItem(props) { }) {
const { detailUrl, editUrl, host, isSelected, onSelect } = props;
const recentPlaybookJobs = host.summary_fields.recent_jobs.map(job => ({
...job,
type: 'job',
}));
const labelId = `check-action-${host.id}`; const labelId = `check-action-${host.id}`;
return ( return (
<DataListItem key={host.id} aria-labelledby={labelId} id={`${host.id}`}> <Tr id={`host-row-${host.id}`}>
<DataListItemRow> <Td
<DataListCheck select={{
id={`select-host-${host.id}`} rowIndex,
checked={isSelected} isSelected,
onChange={onSelect} onSelect,
aria-labelledby={labelId} }}
/> />
<DataListItemCells <Td id={labelId} dataLabel={t`Name`}>
dataListCells={[ <Link to={`${detailUrl}`}>
<DataListCell key="divider"> <b>{host.name}</b>
<Link to={`${detailUrl}`}> </Link>
<b>{host.name}</b> </Td>
</Link> <ActionsTd dataLabel={t`Actions`} gridColumns="auto 40px">
</DataListCell>, <HostToggle host={host} />
<DataListCell key="recentJobs"> {host.summary_fields.user_capabilities?.edit && (
<Sparkline jobs={recentPlaybookJobs} /> <Tooltip content={t`Edit Host`} position="top">
</DataListCell>, <Button
]} ouiaId={`${host.id}-edit-button`}
/> variant="plain"
<DataListAction component={Link}
aria-label={t`actions`} to={`${editUrl}`}
aria-labelledby={labelId} >
id={labelId} <PencilAltIcon />
> </Button>
<HostToggle host={host} /> </Tooltip>
{host.summary_fields.user_capabilities?.edit && ( )}
<Tooltip content={t`Edit Host`} position="top"> </ActionsTd>
<Button </Tr>
ouiaId={`${host.id}-edit-button`}
variant="plain"
component={Link}
to={`${editUrl}`}
>
<PencilAltIcon />
</Button>
</Tooltip>
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -8,7 +8,11 @@ import useRequest, { useDeleteItems } from '../../../util/useRequest';
import AlertModal from '../../../components/AlertModal'; import AlertModal from '../../../components/AlertModal';
import DataListToolbar from '../../../components/DataListToolbar'; import DataListToolbar from '../../../components/DataListToolbar';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import PaginatedDataList, { import PaginatedTable, {
HeaderRow,
HeaderCell,
} from '../../../components/PaginatedTable';
import {
ToolbarAddButton, ToolbarAddButton,
ToolbarDeleteButton, ToolbarDeleteButton,
} from '../../../components/PaginatedDataList'; } from '../../../components/PaginatedDataList';
@@ -105,35 +109,21 @@ function InventoryHostList() {
return ( return (
<> <>
<PaginatedDataList <PaginatedTable
contentError={contentError} contentError={contentError}
hasContentLoading={isLoading || isDeleteLoading || isAdHocLaunchLoading} hasContentLoading={isLoading || isDeleteLoading || isAdHocLaunchLoading}
items={hosts} items={hosts}
itemCount={hostCount} itemCount={hostCount}
pluralizedItemName={t`Hosts`} pluralizedItemName={t`Hosts`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarColumns={[
{
name: t`Name`,
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: t`Modified`,
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: t`Created`,
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow>
}
renderToolbar={props => ( renderToolbar={props => (
<DataListToolbar <DataListToolbar
{...props} {...props}
@@ -164,14 +154,15 @@ function InventoryHostList() {
]} ]}
/> />
)} )}
renderItem={o => ( renderRow={(host, index) => (
<InventoryHostItem <InventoryHostItem
key={o.id} key={host.id}
host={o} host={host}
detailUrl={`/inventories/inventory/${id}/hosts/${o.id}/details`} detailUrl={`/inventories/inventory/${id}/hosts/${host.id}/details`}
editUrl={`/inventories/inventory/${id}/hosts/${o.id}/edit`} editUrl={`/inventories/inventory/${id}/hosts/${host.id}/edit`}
isSelected={selected.some(row => row.id === o.id)} isSelected={selected.some(row => row.id === host.id)}
onSelect={() => handleSelect(o)} onSelect={() => handleSelect(host)}
rowIndex={index}
/> />
)} )}
emptyStateControls={ emptyStateControls={