@@ -62,20 +114,33 @@ class HostContainer extends Component {
return (
-
-
-
+ {/* TODO: find a way to move these controls into the table component */}
+
+
+
+
+
+
+
+ Search by hostname, UUID, serial number, or IPv4
+
+ {renderEditColumnsModal()}
);
}
diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostContainer/HostTableConfig.jsx b/frontend/pages/hosts/ManageHostsPage/components/HostContainer/HostTableConfig.jsx
new file mode 100644
index 0000000000..2bb0ce9935
--- /dev/null
+++ b/frontend/pages/hosts/ManageHostsPage/components/HostContainer/HostTableConfig.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+
+import HeaderCell from '../HeaderCell/HeaderCell';
+import LinkCell from '../LinkCell/LinkCell';
+import StatusCell from '../StatusCell/StatusCell';
+import TextCell from '../TextCell/TextCell';
+import { humanHostMemory, humanHostUptime, humanHostLastSeen } from '../../../../../kolide/helpers';
+
+const hostDataHeaders = [
+ {
+ title: 'Hostname',
+ Header: cellProps =>
,
+ accessor: 'hostname',
+ Cell: cellProps =>
,
+ canHide: false,
+ },
+ {
+ title: 'Status',
+ Header: 'Status',
+ disableSortBy: true,
+ accessor: 'status',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'OS',
+ Header: cellProps =>
,
+ accessor: 'os_version',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'Osquery',
+ Header: cellProps =>
,
+ accessor: 'osquery_version',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'IPv4',
+ Header: cellProps =>
,
+ accessor: 'primary_ip',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'Last Seen',
+ Header: cellProps =>
,
+ accessor: 'seen_time',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'UUID',
+ Header: cellProps =>
,
+ accessor: 'uuid',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'Uptime',
+ Header: cellProps =>
,
+ accessor: 'uptime',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'CPU',
+ Header: 'CPU',
+ disableSortBy: true,
+ accessor: 'host_cpu',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'Memory',
+ Header: cellProps =>
,
+ accessor: 'memory',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'MAC Address',
+ Header: cellProps =>
,
+ accessor: 'primary_mac',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'Serial Number',
+ Header: cellProps =>
,
+ accessor: 'hardware_serial',
+ Cell: cellProps =>
,
+ },
+ {
+ title: 'Hardware Model',
+ Header: cellProps =>
,
+ accessor: 'hardware_model',
+ Cell: cellProps =>
,
+ },
+];
+
+const defaultHiddenColumns = [
+ 'primary_mac',
+ 'host_cpu',
+ 'memory',
+ 'uptime',
+ 'uuid',
+ 'seen_time',
+ 'hardware_model',
+ 'hardware_serial',
+];
+
+export { hostDataHeaders, defaultHiddenColumns };
diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostContainer/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/HostContainer/_styles.scss
index f552cbadc4..6579c18582 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/HostContainer/_styles.scss
+++ b/frontend/pages/hosts/ManageHostsPage/components/HostContainer/_styles.scss
@@ -3,15 +3,8 @@
&--no-hosts {
display: flex;
flex-direction: column;
- justify-content: center;
- padding-top: 35px;
- font-size: 15px;
- font-weight: $regular;
- line-height: 2;
- letter-spacing: normal;
- color: rgba(32, 37, 50, 0.66);
- border-top: 1px solid $ui-borders;
- margin-top: $pad-half;
+ align-items: center;
+ margin-top: 80px;
h1 {
font-size: $large;
@@ -47,8 +40,13 @@
&__inner {
display: flex;
- align-items: center;
- justify-content: center;
+ flex-direction: row;
+
+ h1 {
+ font-size: $small;
+ font-weight: $bold;
+ margin-bottom: $pad-medium;
+ }
img {
width: 176px;
@@ -61,6 +59,12 @@
font-size: $x-small;
margin: 0;
}
+
+ .no-filter-results {
+ display: flex;
+ flex-direction: column;
+ width: 350px;
+ }
}
.host-pagination__pager-wrap {
@@ -96,11 +100,35 @@
}
}
+ &__table-controls {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ }
+
+ &__edit-columns-button {
+ display: flex;
+ align-items: center;
+ font-size: $x-small;
+ color: $core-blue;
+
+ img {
+ width: 20px;
+ margin-right: $pad-half;
+ }
+ }
+
+ &__edit-columns-button:hover {
+ cursor: pointer;
+ text-decoration: underline;
+ color: $core-blue-over;
+ }
+
&__search-input {
position: relative;
color: $core-dark-blue-grey;
width: 344px;
- margin-left: auto;
+ margin-left: $pad-medium;
.host-container__input-wrapper {
margin-bottom: 0;
diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/HostsDataTable.jsx b/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/HostsDataTable.jsx
index 8c0ca618cd..646fdfa69b 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/HostsDataTable.jsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/HostsDataTable.jsx
@@ -3,18 +3,11 @@ import PropTypes from 'prop-types';
import { useTable, useGlobalFilter, useSortBy, useAsyncDebounce } from 'react-table';
import { useSelector, useDispatch } from 'react-redux';
-
// TODO: move this file closer to HostsDataTable
-import { humanHostMemory, humanHostUptime } from 'kolide/helpers';
import { getHostTableData } from 'redux/nodes/components/ManageHostsPage/actions';
import Spinner from 'components/loaders/Spinner';
import HostPagination from 'components/hosts/HostPagination';
-
-import HeaderCell from '../HeaderCell/HeaderCell';
-import TextCell from '../TextCell/TextCell';
-import StatusCell from '../StatusCell/StatusCell';
-import LinkCell from '../LinkCell/LinkCell';
import scrollToTop from '../../../../../utilities/scroll_to_top';
// TODO: pass in as props
@@ -42,6 +35,8 @@ const HostsDataTable = (props) => {
// component cannot access the router state.
selectedFilter,
searchQuery,
+ hiddenColumns,
+ tableColumns,
} = props;
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
@@ -59,20 +54,9 @@ const HostsDataTable = (props) => {
const pageIndexChangeRef = useRef();
- // TODO: maybe pass as props?
const columns = useMemo(() => {
- return [
- { Header: cellProps =>
, accessor: 'hostname', Cell: cellProps =>
},
- { Header: 'Status', disableSortBy: true, accessor: 'status', Cell: cellProps =>
},
- { Header: cellProps =>
, accessor: 'os_version', Cell: cellProps =>
},
- { Header: cellProps =>
, accessor: 'osquery_version', Cell: cellProps =>
},
- { Header: cellProps =>
, accessor: 'primary_ip', Cell: cellProps =>
},
- { Header: cellProps =>
, accessor: 'primary_mac', Cell: cellProps =>
},
- { Header: 'CPU', disableSortBy: true, accessor: 'host_cpu', Cell: cellProps =>
},
- { Header: cellProps =>
, accessor: 'memory', Cell: cellProps =>
},
- { Header: cellProps =>
, accessor: 'uptime', Cell: cellProps =>
},
- ];
- }, []);
+ return tableColumns;
+ }, [tableColumns]);
const data = useMemo(() => {
return hostAPIOrder.map((id) => {
@@ -85,13 +69,16 @@ const HostsDataTable = (props) => {
rows,
prepareRow,
setGlobalFilter,
+ setHiddenColumns,
state: tableState,
} = useTable(
{ columns,
data,
initialState: {
sortBy: [{ id: DEFAULT_SORT_KEY, desc: true }],
+ hiddenColumns,
},
+ autoResetHiddenColumns: false,
disableMultiSort: true,
manualGlobalFilter: true,
manualSortBy: true,
@@ -110,28 +97,34 @@ const HostsDataTable = (props) => {
const onPaginationChange = useCallback((newPage) => {
if (newPage > pageIndex) {
- // pageIndexChangeRef.current = pageIndex;
setPageIndex(pageIndex + 1);
} else {
- // pageIndpageIndexChangeRefexRef.current = pageIndex;
setPageIndex(pageIndex - 1);
}
pageIndexChangeRef.current = true;
scrollToTop();
}, [pageIndex, setPageIndex]);
- // Since searchQuery is feed in from the parent, we want to debounce the globalfilter change
+ // Since searchQuery is passed in from the parent, we want to debounce the globalFilter change
// when we see it change.
useEffect(() => {
debouncedGlobalFilter(searchQuery);
}, [debouncedGlobalFilter, searchQuery]);
- // Any changes to these relevent table search params will fire off an action to get the new
+ // Track hidden columns changing and update the table accordingly.
+ useEffect(() => {
+ setHiddenColumns(hiddenColumns);
+ }, [setHiddenColumns, hiddenColumns]);
+
+ // Any changes to these relevant table search params will fire off an action to get the new
// hosts data.
useEffect(() => {
if (pageIndexChangeRef.current) { // the pageIndex has changed
dispatch(getHostTableData(pageIndex, pageSize, selectedFilter, globalFilter, sortBy));
- } else {
+ } else { // something besides pageIndex changed. we want to get results starting at the first page
+ // NOTE: currently this causes the request to fire twice if the user is not on the first page
+ // of results. Need to come back to this and figure out how to get it to
+ // only fire once.
setPageIndex(0);
dispatch(getHostTableData(0, pageSize, selectedFilter, globalFilter, sortBy));
}
@@ -144,18 +137,11 @@ const HostsDataTable = (props) => {
return (
-
-
No hosts match the current search criteria
+
+
No hosts match the current criteria
Expecting to see new hosts? Try again in a few seconds as the system catches up
-
-
);
}
@@ -166,6 +152,11 @@ const HostsDataTable = (props) => {
{generateHostCountText(pageIndex, pageSize, rows.length)}
+ {loadingHosts &&
+
+
+
+ }
{headerGroups.map(headerGroup => (
@@ -179,22 +170,20 @@ const HostsDataTable = (props) => {
))}
- {loadingHosts
- ? |
- : rows.map((row) => {
- prepareRow(row);
- return (
-
- {row.cells.map((cell) => {
- return (
- |
- {cell.render('Cell')}
- |
- );
- })}
-
- );
- })
+ {rows.map((row) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell) => {
+ return (
+ |
+ {cell.render('Cell')}
+ |
+ );
+ })}
+
+ );
+ })
}
@@ -213,6 +202,8 @@ const HostsDataTable = (props) => {
HostsDataTable.propTypes = {
selectedFilter: PropTypes.string,
searchQuery: PropTypes.string,
+ tableColumns: PropTypes.arrayOf(PropTypes.object), // TODO: create proper interface for this
+ hiddenColumns: PropTypes.arrayOf(PropTypes.string),
};
export default HostsDataTable;
diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/_styles.scss
index 5c39b7ef32..b50104c81a 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/_styles.scss
+++ b/frontend/pages/hosts/ManageHostsPage/components/HostsDataTable/_styles.scss
@@ -86,5 +86,15 @@
}
}
}
+
+ .loading-overlay {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: rgba(255, 255, 255, 0.8);
+ z-index: 1;
+ }
}
}