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 ? (
+
+
+
+ ) : (
+ ''
+ )}
+
+
);
}