diff --git a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx
index 964806f05e..ca1fd60143 100644
--- a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx
+++ b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx
@@ -57,23 +57,25 @@ function RoutedTabs(props) {
return (
- {tabsArray.map(tab => (
-
- {tab.name}
- >
- ) : (
- tab.name
- )
- }
- />
- ))}
+ {tabsArray
+ .filter(tab => tab.isNestedTab || !tab.name.startsWith('Return'))
+ .map(tab => (
+
+ {tab.name}
+ >
+ ) : (
+ tab.name
+ )
+ }
+ />
+ ))}
);
}
diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx
index 7fc96f9ea5..cbd6ff6659 100644
--- a/awx/ui_next/src/screens/Host/Host.jsx
+++ b/awx/ui_next/src/screens/Host/Host.jsx
@@ -9,6 +9,9 @@ import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError';
import HostFacts from './HostFacts';
import HostDetail from './HostDetail';
+import AlertModal from '@components/AlertModal';
+import ErrorDetail from '@components/ErrorDetail';
+
import HostEdit from './HostEdit';
import HostGroups from './HostGroups';
import HostCompletedJobs from './HostCompletedJobs';
@@ -23,8 +26,16 @@ class Host extends Component {
hasContentLoading: true,
contentError: null,
isInitialized: false,
+ toggleLoading: false,
+ toggleError: null,
+ deletionError: false,
+ isDeleteModalOpen: false,
};
this.loadHost = this.loadHost.bind(this);
+ this.handleHostToggle = this.handleHostToggle.bind(this);
+ this.handleToggleError = this.handleToggleError.bind(this);
+ this.handleHostDelete = this.handleHostDelete.bind(this);
+ this.toggleDeleteModal = this.toggleDeleteModal.bind(this);
}
async componentDidMount() {
@@ -45,15 +56,54 @@ class Host extends Component {
}
}
+ toggleDeleteModal() {
+ const { isDeleteModalOpen } = this.state;
+ this.setState({ isDeleteModalOpen: !isDeleteModalOpen });
+ }
+
+ async handleHostToggle() {
+ const { host } = this.state;
+ this.setState({ toggleLoading: true });
+ try {
+ const { data } = await HostsAPI.update(host.id, {
+ enabled: !host.enabled,
+ });
+ this.setState({ host: data });
+ } catch (err) {
+ this.setState({ toggleError: err });
+ } finally {
+ this.setState({ toggleLoading: null });
+ }
+ }
+
+ async handleHostDelete() {
+ const { host } = this.state;
+ const { match, history } = this.props;
+
+ this.setState({ hasContentLoading: true });
+ try {
+ await HostsAPI.destroy(host.id);
+ this.setState({ hasContentLoading: false });
+ history.push(`/inventories/inventory/${match.params.id}/hosts`);
+ } catch (err) {
+ this.setState({ deletionError: err });
+ }
+ }
+
async loadHost() {
- const { match, setBreadcrumb } = this.props;
- const id = parseInt(match.params.id, 10);
+ const { match, setBreadcrumb, history, inventory } = this.props;
this.setState({ contentError: null, hasContentLoading: true });
try {
- const { data } = await HostsAPI.readDetail(id);
- setBreadcrumb(data);
+ const { data } = await HostsAPI.readDetail(
+ match.params.hostId || match.params.id
+ );
this.setState({ host: data });
+
+ if (history.location.pathname.startsWith('/hosts')) {
+ setBreadcrumb(data);
+ }
+ setBreadcrumb(inventory, data);
} catch (err) {
this.setState({ contentError: err });
} finally {
@@ -61,15 +111,44 @@ class Host extends Component {
}
}
+ handleToggleError() {
+ this.setState({ toggleError: false });
+ }
+
render() {
const { location, match, history, i18n } = this.props;
-
- const { host, contentError, hasContentLoading, isInitialized } = this.state;
-
+ const {
+ deletionError,
+ host,
+ isDeleteModalOpen,
+ toggleError,
+ hasContentLoading,
+ toggleLoading,
+ isInitialized,
+ contentError,
+ } = this.state;
const tabsArray = [
- { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
- { name: i18n._(t`Facts`), link: `${match.url}/facts`, id: 1 },
- { name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 },
+ {
+ name: i18n._(t`Return to Hosts`),
+ link: `/inventories/inventory/${match.params.id}/hosts`,
+ id: 99,
+ isNestedTab: !history.location.pathname.startsWith('/hosts'),
+ },
+ {
+ name: i18n._(t`Details`),
+ link: `${match.url}/details`,
+ id: 0,
+ },
+ {
+ name: i18n._(t`Facts`),
+ link: `${match.url}/facts`,
+ id: 1,
+ },
+ {
+ name: i18n._(t`Groups`),
+ link: `${match.url}/groups`,
+ id: 2,
+ },
{
name: i18n._(t`Completed Jobs`),
link: `${match.url}/completed_jobs`,
@@ -117,62 +196,99 @@ class Host extends Component {
);
}
-
return (
-
-
- {cardHeader}
-
-
- {host && (
+ <>
+
+
+ {cardHeader}
+
+
+ {host && (
+ }
+ />
+ )}
+ {host && (
+ (
+
+ )}
+ />
+ )}
+ {host && (
+ }
+ />
+ )}
+ {host && (
+ }
+ />
+ )}
+ {host && (
+ }
+ />
+ )}
}
+ key="not-found"
+ path="*"
+ render={() =>
+ !hasContentLoading && (
+
+ {match.params.id && (
+
+ {i18n._(`View Host Details`)}
+
+ )}
+
+ )
+ }
/>
- )}
- {host && (
- }
- />
- )}
- {host && (
- }
- />
- )}
- {host && (
- }
- />
- )}
- {host && (
- }
- />
- )}
-
- !hasContentLoading && (
-
- {match.params.id && (
-
- {i18n._(`View Host Details`)}
-
- )}
-
- )
- }
- />
- ,
-
-
-
+ ,
+
+
+
+ {deletionError && (
+ this.setState({ deletionError: false })}
+ >
+ {i18n._(t`Failed to delete ${host.name}.`)}
+
+
+ )}
+ >
);
}
}
diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx
index db62e11f60..91f8416176 100644
--- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx
+++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx
@@ -7,14 +7,127 @@ import { Button } from '@patternfly/react-core';
import { CardBody, CardActionsRow } from '@components/Card';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput';
+import { Sparkline } from '@components/Sparkline';
+import Switch from '@components/Switch';
function HostDetail({ host, i18n }) {
- const { created, description, id, modified, name, summary_fields } = host;
+const ActionButtonWrapper = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 20px;
+ & > :not(:first-child) {
+ margin-left: 20px;
+ }
+`;
+function HostDetail({
+ host,
+ history,
+ isDeleteModalOpen,
+ match,
+ i18n,
+ toggleError,
+ toggleLoading,
+ onHostDelete,
+ onToggleDeleteModal,
+ onToggleError,
+ onHandleHostToggle,
+}) {
+ const { created, description, id, modified, name, summary_fields } = host;
+ let createdBy = '';
+ if (created) {
+ if (summary_fields.created_by && summary_fields.created_by.username) {
+ createdBy = (
+
+ {i18n._(t`${formatDateString(created)} by `)}{' '}
+
+ {summary_fields.created_by.username}
+
+
+ );
+ } else {
+ createdBy = formatDateString(created);
+ }
+ }
+
+ let modifiedBy = '';
+ if (modified) {
+ if (summary_fields.modified_by && summary_fields.modified_by.username) {
+ modifiedBy = (
+
+ {i18n._(t`${formatDateString(modified)} by`)}{' '}
+
+ {summary_fields.modified_by.username}
+
+
+ );
+ } else {
+ modifiedBy = formatDateString(modified);
+ }
+ }
+ if (toggleError && !toggleLoading) {
+ return (
+
+ {i18n._(t`Failed to toggle host.`)}
+
+
+ );
+ }
+ if (isDeleteModalOpen) {
+ return (
+ onToggleDeleteModal()}
+ >
+ {i18n._(t`Are you sure you want to delete:`)}
+
+ {host.name}
+
+
+
+
+
+
+ );
+ }
return (
+
+ }
+ label={i18n._(t`Activity`)}
+ />
{summary_fields.inventory && (
)}
-
-
+
+
@@ -54,7 +159,11 @@ function HostDetail({ host, i18n }) {
diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx
index 749c512171..5562a5cc05 100644
--- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx
+++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx
@@ -50,8 +50,7 @@ describe('', () => {
test('should show edit button for users with edit permission', async () => {
const wrapper = mountWithContexts();
- // VariablesDetail has two buttons
- const editButton = wrapper.find('Button').at(2);
+ const editButton = wrapper.find('Button[aria-label="edit"]');
expect(editButton.text()).toEqual('Edit');
expect(editButton.prop('to')).toBe('/hosts/1/edit');
});
@@ -61,7 +60,6 @@ describe('', () => {
readOnlyHost.summary_fields.user_capabilities.edit = false;
const wrapper = mountWithContexts();
await waitForElement(wrapper, 'HostDetail');
- // VariablesDetail has two buttons
- expect(wrapper.find('Button').length).toBe(2);
+ expect(wrapper.find('Button[aria-label="edit"]').length).toBe(0);
});
});
diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.jsx
index 4231ef2cc0..978ff2ba59 100644
--- a/awx/ui_next/src/screens/Host/HostList/HostList.jsx
+++ b/awx/ui_next/src/screens/Host/HostList/HostList.jsx
@@ -238,7 +238,7 @@ class HostsList extends Component {
detailUrl={`${match.url}/${o.id}`}
isSelected={selected.some(row => row.id === o.id)}
onSelect={() => this.handleSelect(o)}
- toggleHost={this.handleHostToggle}
+ onToggleHost={this.handleHostToggle}
toggleLoading={toggleLoading === o.id}
/>
)}
diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx
index 9123136749..6e74f9da3b 100644
--- a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx
+++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx
@@ -34,7 +34,7 @@ class HostListItem extends React.Component {
isSelected,
onSelect,
detailUrl,
- toggleHost,
+ onToggleHost,
toggleLoading,
i18n,
} = this.props;
@@ -93,7 +93,7 @@ class HostListItem extends React.Component {
toggleLoading ||
!host.summary_fields.user_capabilities.edit
}
- onChange={() => toggleHost(host)}
+ onChange={() => onToggleHost(host)}
aria-label={i18n._(t`Toggle host`)}
/>
diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx
index d9e5987661..285f71fba4 100644
--- a/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx
+++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx
@@ -4,7 +4,7 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
import HostsListItem from './HostListItem';
-let toggleHost;
+let onToggleHost;
const mockHost = {
id: 1,
@@ -24,7 +24,7 @@ const mockHost = {
describe('', () => {
beforeEach(() => {
- toggleHost = jest.fn();
+ onToggleHost = jest.fn();
});
afterEach(() => {
@@ -38,7 +38,7 @@ describe('', () => {
detailUrl="/host/1"
onSelect={() => {}}
host={mockHost}
- toggleHost={toggleHost}
+ onToggleHost={onToggleHost}
/>
);
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
@@ -52,7 +52,7 @@ describe('', () => {
detailUrl="/host/1"
onSelect={() => {}}
host={copyMockHost}
- toggleHost={toggleHost}
+ onToggleHost={onToggleHost}
/>
);
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
@@ -64,7 +64,7 @@ describe('', () => {
detailUrl="/host/1"
onSelect={() => {}}
host={mockHost}
- toggleHost={toggleHost}
+ onToggleHost={onToggleHost}
/>
);
wrapper
@@ -72,7 +72,7 @@ describe('', () => {
.first()
.find('input')
.simulate('change');
- expect(toggleHost).toHaveBeenCalledWith(mockHost);
+ expect(onToggleHost).toHaveBeenCalledWith(mockHost);
});
test('handles toggle click when host is disabled', () => {
@@ -82,7 +82,7 @@ describe('', () => {
detailUrl="/host/1"
onSelect={() => {}}
host={mockHost}
- toggleHost={toggleHost}
+ onToggleHost={onToggleHost}
/>
);
wrapper
@@ -90,6 +90,6 @@ describe('', () => {
.first()
.find('input')
.simulate('change');
- expect(toggleHost).toHaveBeenCalledWith(mockHost);
+ expect(onToggleHost).toHaveBeenCalledWith(mockHost);
});
});
diff --git a/awx/ui_next/src/screens/Host/Hosts.jsx b/awx/ui_next/src/screens/Host/Hosts.jsx
index 0a17916223..59bb53bd06 100644
--- a/awx/ui_next/src/screens/Host/Hosts.jsx
+++ b/awx/ui_next/src/screens/Host/Hosts.jsx
@@ -46,14 +46,17 @@ class Hosts extends Component {
};
render() {
- const { match, history, location } = this.props;
+ const { match, history, location, inventory } = this.props;
const { breadcrumbConfig } = this.state;
return (
- } />
+ }
+ />
(
@@ -64,6 +67,7 @@ class Hosts extends Component {
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
me={me || {}}
+ inventory={inventory}
/>
)}
diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx
index 4253078486..49264336b5 100644
--- a/awx/ui_next/src/screens/Inventory/Inventories.jsx
+++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx
@@ -27,7 +27,7 @@ class Inventories extends Component {
};
}
- setBreadCrumbConfig = (inventory, group) => {
+ setBreadCrumbConfig = (inventory, nestedResource) => {
const { i18n } = this.props;
if (!inventory) {
return;
@@ -51,21 +51,39 @@ class Inventories extends Component {
[`/inventories/${inventoryKind}/${inventory.id}/completed_jobs`]: i18n._(
t`Completed Jobs`
),
- [`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
+ [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
+ nestedResource.id}`]: i18n._(
+ t`${nestedResource && nestedResource.name}`
+ ),
+ [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
+ nestedResource.id}/details`]: i18n._(t`Details`),
+ [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
+ nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
t`Create New Host`
),
- [`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`),
- [`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`),
- [`/inventories/inventory/${inventory.id}/groups/add`]: i18n._(
+ [`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._(
+ t`Sources`
+ ),
+ [`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._(
+ t`Groups`
+ ),
+ [`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._(
t`Create New Group`
),
- [`/inventories/inventory/${inventory.id}/groups/${group &&
- group.id}`]: `${group && group.name}`,
- [`/inventories/inventory/${inventory.id}/groups/${group &&
- group.id}/details`]: i18n._(t`Group Details`),
- [`/inventories/inventory/${inventory.id}/groups/${group &&
- group.id}/edit`]: i18n._(t`Edit Details`),
+ [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
+ nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
+ [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
+ nestedResource.id}/details`]: i18n._(t`Group Details`),
+ [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
+ nestedResource.id}/edit`]: i18n._(t`Edit Details`),
+ [`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
+ [`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._(
+ t`Sources`
+ ),
+ [`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._(
+ t`Groups`
+ ),
};
this.setState({ breadcrumbConfig });
};
diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx
index e632610660..c2cb831331 100644
--- a/awx/ui_next/src/screens/Inventory/Inventory.jsx
+++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx
@@ -8,14 +8,15 @@ import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList';
+import ContentLoading from '@components/ContentLoading';
import InventoryDetail from './InventoryDetail';
-import InventoryHosts from './InventoryHosts';
-import InventoryHostAdd from './InventoryHostAdd';
+
import InventoryGroups from './InventoryGroups';
import InventoryCompletedJobs from './InventoryCompletedJobs';
import InventorySources from './InventorySources';
import { InventoriesAPI } from '@api';
import InventoryEdit from './InventoryEdit';
+import InventoryHosts from './InventoryHosts/InventoryHosts';
function Inventory({ history, i18n, location, match, setBreadcrumb }) {
const [contentError, setContentError] = useState(null);
@@ -61,10 +62,14 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
if (
location.pathname.endsWith('edit') ||
location.pathname.endsWith('add') ||
- location.pathname.includes('groups/')
+ location.pathname.includes('groups/') ||
+ history.location.pathname.includes(`/hosts/`)
) {
cardHeader = null;
}
+ if (hasContentLoading) {
+ return ;
+ }
if (!hasContentLoading && contentError) {
return (
@@ -111,9 +116,16 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
render={() => }
/>,
}
+ key="hosts"
+ path="/inventories/inventory/:id/hosts"
+ render={() => (
+
+ )}
/>,
)}
/>,
- }
- />,
;
diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx
new file mode 100644
index 0000000000..df4d7783fd
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx
@@ -0,0 +1,226 @@
+import React, { useEffect, useState } from 'react';
+import { withRouter } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { getQSConfig, parseQueryString } from '@util/qs';
+import { InventoriesAPI, HostsAPI } from '@api';
+
+import AlertModal from '@components/AlertModal';
+import DataListToolbar from '@components/DataListToolbar';
+import ErrorDetail from '@components/ErrorDetail';
+import PaginatedDataList, {
+ ToolbarAddButton,
+ ToolbarDeleteButton,
+} from '@components/PaginatedDataList';
+import InventoryHostItem from './InventoryHostItem';
+
+const QS_CONFIG = getQSConfig('host', {
+ page: 1,
+ page_size: 20,
+ order_by: 'name',
+});
+
+function InventoryHostList({ i18n, location, match }) {
+ const [actions, setActions] = useState(null);
+ const [contentError, setContentError] = useState(null);
+ const [deletionError, setDeletionError] = useState(null);
+ const [hostCount, setHostCount] = useState(0);
+ const [hosts, setHosts] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [selected, setSelected] = useState([]);
+ const [toggleError, setToggleError] = useState(null);
+ const [toggleLoading, setToggleLoading] = useState(null);
+
+ const fetchHosts = (id, queryString) => {
+ const params = parseQueryString(QS_CONFIG, queryString);
+ return InventoriesAPI.readHosts(id, params);
+ };
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const [
+ {
+ data: { count, results },
+ },
+ {
+ data: { actions: optionActions },
+ },
+ ] = await Promise.all([
+ fetchHosts(match.params.id, location.search),
+ InventoriesAPI.readOptions(),
+ ]);
+
+ setHosts(results);
+ setHostCount(count);
+ setActions(optionActions);
+ } catch (error) {
+ setContentError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ fetchData();
+ }, [match.params.id, location]);
+
+ const handleSelectAll = isSelected => {
+ setSelected(isSelected ? [...hosts] : []);
+ };
+
+ const handleSelect = row => {
+ if (selected.some(s => s.id === row.id)) {
+ setSelected(selected.filter(s => s.id !== row.id));
+ } else {
+ setSelected(selected.concat(row));
+ }
+ };
+
+ const handleDelete = async () => {
+ setIsLoading(true);
+
+ try {
+ await Promise.all(selected.map(host => HostsAPI.destroy(host.id)));
+ } catch (error) {
+ setDeletionError(error);
+ } finally {
+ setSelected([]);
+ try {
+ const {
+ data: { count, results },
+ } = await fetchHosts(match.params.id, location.search);
+
+ setHosts(results);
+ setHostCount(count);
+ } catch (error) {
+ setContentError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ const handleToggle = async hostToToggle => {
+ setToggleLoading(hostToToggle.id);
+
+ try {
+ const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
+ enabled: !hostToToggle.enabled,
+ });
+
+ setHosts(
+ hosts.map(host => (host.id === updatedHost.id ? updatedHost : host))
+ );
+ } catch (error) {
+ setToggleError(error);
+ } finally {
+ setToggleLoading(null);
+ }
+ };
+
+ const canAdd =
+ actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
+ const isAllSelected = selected.length > 0 && selected.length === hosts.length;
+
+ return (
+ <>
+ (
+ ,
+ canAdd && (
+
+ ),
+ ]}
+ />
+ )}
+ renderItem={o => (
+ row.id === o.id)}
+ onSelect={() => handleSelect(o)}
+ toggleHost={handleToggle}
+ toggleLoading={toggleLoading === o.id}
+ />
+ )}
+ emptyStateControls={
+ canAdd && (
+
+ )
+ }
+ />
+
+ {toggleError && !toggleLoading && (
+ setToggleError(false)}
+ >
+ {i18n._(t`Failed to toggle host.`)}
+
+
+ )}
+
+ {deletionError && (
+ setDeletionError(null)}
+ >
+ {i18n._(t`Failed to delete one or more hosts.`)}
+
+
+ )}
+ >
+ );
+}
+
+export default withI18n()(withRouter(InventoryHostList));
diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx
similarity index 94%
rename from awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.test.jsx
rename to awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx
index 715413c81b..d35d4da489 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { InventoriesAPI, HostsAPI } from '@api';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
-import InventoryHosts from './InventoryHosts';
+import InventoryHostList from './InventoryHostList';
import mockInventory from '../shared/data.inventory.json';
jest.mock('@api');
@@ -62,7 +62,7 @@ const mockHosts = [
},
];
-describe('', () => {
+describe('', () => {
let wrapper;
beforeEach(async () => {
@@ -81,7 +81,7 @@ describe('', () => {
},
});
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts();
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
@@ -91,7 +91,7 @@ describe('', () => {
});
test('initially renders successfully', () => {
- expect(wrapper.find('InventoryHosts').length).toBe(1);
+ expect(wrapper.find('InventoryHostList').length).toBe(1);
});
test('should fetch hosts from api and render them in the list', async () => {
@@ -261,7 +261,9 @@ describe('', () => {
},
});
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts(
+
+ );
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
@@ -272,7 +274,9 @@ describe('', () => {
Promise.reject(new Error())
);
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts(
+
+ );
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx
index 9e96793e3f..c88362686a 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx
@@ -1,228 +1,46 @@
-import React, { useEffect, useState } from 'react';
-import { withRouter } from 'react-router-dom';
-import { withI18n } from '@lingui/react';
-import { t } from '@lingui/macro';
-import { getQSConfig, parseQueryString } from '@util/qs';
-import { InventoriesAPI, HostsAPI } from '@api';
+import React from 'react';
+import { Switch, Route, withRouter } from 'react-router-dom';
-import AlertModal from '@components/AlertModal';
-import DataListToolbar from '@components/DataListToolbar';
-import ErrorDetail from '@components/ErrorDetail';
-import PaginatedDataList, {
- ToolbarAddButton,
- ToolbarDeleteButton,
-} from '@components/PaginatedDataList';
-import InventoryHostItem from './InventoryHostItem';
-
-const QS_CONFIG = getQSConfig('host', {
- page: 1,
- page_size: 20,
- order_by: 'name',
-});
-
-function InventoryHosts({ i18n, location, match }) {
- const [actions, setActions] = useState(null);
- const [contentError, setContentError] = useState(null);
- const [deletionError, setDeletionError] = useState(null);
- const [hostCount, setHostCount] = useState(0);
- const [hosts, setHosts] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
- const [selected, setSelected] = useState([]);
- const [toggleError, setToggleError] = useState(null);
- const [toggleLoading, setToggleLoading] = useState(null);
-
- const fetchHosts = (id, queryString) => {
- const params = parseQueryString(QS_CONFIG, queryString);
- return InventoriesAPI.readHosts(id, params);
- };
-
- useEffect(() => {
- async function fetchData() {
- try {
- const [
- {
- data: { count, results },
- },
- {
- data: { actions: optionActions },
- },
- ] = await Promise.all([
- fetchHosts(match.params.id, location.search),
- InventoriesAPI.readOptions(),
- ]);
-
- setHosts(results);
- setHostCount(count);
- setActions(optionActions);
- } catch (error) {
- setContentError(error);
- } finally {
- setIsLoading(false);
- }
- }
-
- fetchData();
- }, [match.params.id, location]);
-
- const handleSelectAll = isSelected => {
- setSelected(isSelected ? [...hosts] : []);
- };
-
- const handleSelect = row => {
- if (selected.some(s => s.id === row.id)) {
- setSelected(selected.filter(s => s.id !== row.id));
- } else {
- setSelected(selected.concat(row));
- }
- };
-
- const handleDelete = async () => {
- setIsLoading(true);
-
- try {
- await Promise.all(selected.map(host => HostsAPI.destroy(host.id)));
- } catch (error) {
- setDeletionError(error);
- } finally {
- setSelected([]);
- try {
- const {
- data: { count, results },
- } = await fetchHosts(match.params.id, location.search);
-
- setHosts(results);
- setHostCount(count);
- } catch (error) {
- setContentError(error);
- } finally {
- setIsLoading(false);
- }
- }
- };
-
- const handleToggle = async hostToToggle => {
- setToggleLoading(hostToToggle.id);
-
- try {
- const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
- enabled: !hostToToggle.enabled,
- });
-
- setHosts(
- hosts.map(host => (host.id === updatedHost.id ? updatedHost : host))
- );
- } catch (error) {
- setToggleError(error);
- } finally {
- setToggleLoading(null);
- }
- };
-
- const canAdd =
- actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
- const isAllSelected = selected.length > 0 && selected.length === hosts.length;
+import Host from '../../Host/Host';
+import InventoryHostList from './InventoryHostList';
+import HostAdd from '../InventoryHostAdd';
+function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) {
return (
- <>
- (
- ,
- canAdd && (
-
- ),
- ]}
- />
- )}
- renderItem={o => (
- row.id === o.id)}
- onSelect={() => handleSelect(o)}
- toggleHost={handleToggle}
- toggleLoading={toggleLoading === o.id}
- />
- )}
- emptyStateControls={
- canAdd && (
-
- )
- }
+
+ }
/>
-
- {toggleError && !toggleLoading && (
- setToggleError(false)}
- >
- {i18n._(t`Failed to toggle host.`)}
-
-
- )}
-
- {deletionError && (
- setDeletionError(null)}
- >
- {i18n._(t`Failed to delete one or more hosts.`)}
-
-
- )}
- >
+ ,
+ (
+
+ )}
+ />
+ ,
+ (
+
+ )}
+ />
+ ,
+
);
}
-export default withI18n()(withRouter(InventoryHosts));
+export default withRouter(InventoryHosts);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js b/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js
index 6d33814f29..0cb4fe95bc 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js
+++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js
@@ -1 +1 @@
-export { default } from './InventoryHosts';
+export { default } from './InventoryHostList';