From a0773a49a476dcdb6c98b4bc4fb7971a5bcb5da7 Mon Sep 17 00:00:00 2001
From: Christina Jenks <christina.jenks@ess.eu>
Date: Mon, 14 Aug 2023 09:55:29 +0000
Subject: [PATCH] CE-1942: convert HostTable to common table

---
 cypress/support/commands.js           |   7 +-
 package-lock.json                     | 184 ++++++++++++++++++++++++++
 package.json                          |   1 +
 src/components/common/Helper.js       |   9 +-
 src/components/host/HostTable.js      |  68 +++++-----
 src/components/host/HostTable.spec.js |  13 +-
 src/hooks/pagination.js               |  57 ++++++++
 src/views/host/HostListView.js        | 112 ++++++++++------
 8 files changed, 360 insertions(+), 91 deletions(-)
 create mode 100644 src/hooks/pagination.js

diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 9142c316..1d4efa7f 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -23,4 +23,9 @@
 //
 // -- This will overwrite an existing command --
 // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
-Cypress.Commands.add("login", () => {cy.setCookie("ce-deploy-auth", "DEADBEEF")});
\ No newline at end of file
+
+// Import React Testing Library commands (select by a11y role etc)
+import "@testing-library/cypress/add-commands";
+
+// import custom auth commands
+Cypress.Commands.add("login", () => {cy.setCookie("ce-deploy-auth", "DEADBEEF")});
diff --git a/package-lock.json b/package-lock.json
index 48f09c3f..6a1b8073 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,6 +48,7 @@
         "@storybook/react": "^6.4.22",
         "@storybook/testing-library": "^0.0.11",
         "@storybook/testing-react": "^1.2.4",
+        "@testing-library/cypress": "^9.0.0",
         "core-js": "^3.22.3",
         "cypress": "^12.8.0",
         "cypress-cucumber-preprocessor": "^4.1.0",
@@ -7582,6 +7583,112 @@
         "url": "https://github.com/sponsors/gregberge"
       }
     },
+    "node_modules/@testing-library/cypress": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-9.0.0.tgz",
+      "integrity": "sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.14.6",
+        "@testing-library/dom": "^8.1.0"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      },
+      "peerDependencies": {
+        "cypress": "^12.0.0"
+      }
+    },
+    "node_modules/@testing-library/cypress/node_modules/@testing-library/dom": {
+      "version": "8.20.1",
+      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz",
+      "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.10.4",
+        "@babel/runtime": "^7.12.5",
+        "@types/aria-query": "^5.0.1",
+        "aria-query": "5.1.3",
+        "chalk": "^4.1.0",
+        "dom-accessibility-api": "^0.5.9",
+        "lz-string": "^1.5.0",
+        "pretty-format": "^27.0.2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@testing-library/cypress/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@testing-library/cypress/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/@testing-library/cypress/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/@testing-library/cypress/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
+    "node_modules/@testing-library/cypress/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@testing-library/cypress/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/@testing-library/dom": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.0.1.tgz",
@@ -45125,6 +45232,83 @@
         "loader-utils": "^2.0.0"
       }
     },
+    "@testing-library/cypress": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-9.0.0.tgz",
+      "integrity": "sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.14.6",
+        "@testing-library/dom": "^8.1.0"
+      },
+      "dependencies": {
+        "@testing-library/dom": {
+          "version": "8.20.1",
+          "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz",
+          "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.10.4",
+            "@babel/runtime": "^7.12.5",
+            "@types/aria-query": "^5.0.1",
+            "aria-query": "5.1.3",
+            "chalk": "^4.1.0",
+            "dom-accessibility-api": "^0.5.9",
+            "lz-string": "^1.5.0",
+            "pretty-format": "^27.0.2"
+          }
+        },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
     "@testing-library/dom": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.0.1.tgz",
diff --git a/package.json b/package.json
index 6a4142db..d2a88d0b 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
     "@storybook/react": "^6.4.22",
     "@storybook/testing-library": "^0.0.11",
     "@storybook/testing-react": "^1.2.4",
+    "@testing-library/cypress": "^9.0.0",
     "core-js": "^3.22.3",
     "cypress": "^12.8.0",
     "cypress-cucumber-preprocessor": "^4.1.0",
diff --git a/src/components/common/Helper.js b/src/components/common/Helper.js
index 0b74973a..f8e079ff 100644
--- a/src/components/common/Helper.js
+++ b/src/components/common/Helper.js
@@ -157,14 +157,7 @@ export function transformHostQuery(query) {
 }
 
 export function noWrapText(text) {
-  return (
-    <Typography
-      noWrap
-      sx={{ maxWidth: "15ch" }}
-    >
-      {text}
-    </Typography>
-  );
+  return <Typography noWrap>{text}</Typography>;
 }
 
 export function extractMainNetwork(interfaces, defaultReturn = "") {
diff --git a/src/components/host/HostTable.js b/src/components/host/HostTable.js
index bdfc8033..5236a2e7 100644
--- a/src/components/host/HostTable.js
+++ b/src/components/host/HostTable.js
@@ -1,20 +1,32 @@
 import React from "react";
-import { Grid, Tooltip } from "@mui/material";
-import { CustomTable } from "../common/table/CustomTable";
+import { Box, Tooltip } from "@mui/material";
+import { Table } from "@ess-ics/ce-ui-common";
 import { HostStatusIcon } from "./HostIcons";
 import { noWrapText } from "../common/Helper";
 import { useNavigate } from "react-router-dom";
 
 const columns = [
-  { id: "bulb", label: "Status", width: "4ch", textAlign: "center" },
-  { id: "host", label: "Host", width: "8ch" },
-  { id: "description", label: "Description", width: "15ch" },
-  { id: "network", label: "Network", width: "10ch" }
+  {
+    field: "bulb",
+    headerName: "Status",
+    flex: 0,
+    headerAlign: "center",
+    align: "center"
+  },
+  { field: "hostName", headerName: "Host" },
+  { field: "description", headerName: "Description" },
+  { field: "network", headerName: "Network" }
 ];
 
 export function rowDescription(description) {
   return (
-    <div className={"shortenLongDataLines"}>
+    <Box
+      sx={{
+        textOverflow: "ellipsis",
+        whiteSpace: "nowrap",
+        overflow: "hidden"
+      }}
+    >
       <Tooltip
         title={description}
         arrow
@@ -22,7 +34,7 @@ export function rowDescription(description) {
       >
         <label>{noWrapText(description)}</label>
       </Tooltip>
-    </div>
+    </Box>
   );
 }
 
@@ -30,17 +42,9 @@ export function createRow(hostContainer) {
   const host = hostContainer.csEntryHost;
   return {
     id: host.id,
-    bulb: (
-      <Grid
-        container
-        direction="column"
-        justifyContent="center"
-        alignItems="center"
-      >
-        <HostStatusIcon host={hostContainer} />
-      </Grid>
-    ),
-    host: noWrapText(host.name),
+    bulb: <HostStatusIcon host={hostContainer} />,
+    hostName: noWrapText(host.name),
+    host: host,
     description: rowDescription(host.description),
     network: noWrapText(host.network),
     discrepancy: hostContainer.alertSeverity === "WARNING",
@@ -50,32 +54,20 @@ export function createRow(hostContainer) {
   };
 }
 
-export function HostTable({
-  hosts,
-  totalCount,
-  lazyParams,
-  setLazyParams,
-  rowsPerPage,
-  loading
-}) {
+export function HostTable({ hosts, pagination, onPage, loading }) {
   const navigate = useNavigate();
 
-  const itemLink = (item) => `/hosts/${item.id}`;
-
-  const onRowClicked = (id, item) => {
-    navigate(itemLink(item));
+  const onRowClick = (params) => {
+    navigate(`/hosts/${params.row?.host?.id}`);
   };
 
   return (
-    <CustomTable
+    <Table
       columns={columns}
       rows={hosts.map((host) => createRow(host))}
-      handleRowClick={onRowClicked}
-      itemLink={itemLink}
-      totalCount={totalCount}
-      lazyParams={lazyParams}
-      setLazyParams={setLazyParams}
-      rowsPerPage={rowsPerPage}
+      onRowClick={onRowClick}
+      pagination={pagination}
+      onPage={onPage}
       loading={loading}
     />
   );
diff --git a/src/components/host/HostTable.spec.js b/src/components/host/HostTable.spec.js
index 9c683501..01b7dd12 100644
--- a/src/components/host/HostTable.spec.js
+++ b/src/components/host/HostTable.spec.js
@@ -19,27 +19,26 @@ describe("HostTable", () => {
     });
 
     it("Has the correct columns", () => {
-      cy.get(".p-column-title").each(($el, index) => {
+      cy.findAllByRole("columnheader").each(($el, index) => {
         console.debug(index, columns[index]);
         cy.wrap($el).contains(columns[index], { matchCase: false });
       });
     });
 
     it("Truncates all text content", () => {
-      cy.get("tbody > tr")
-        .first()
+      cy.findAllByRole("row")
+        .eq(1) // first row is headers, so get next index
         .find(".MuiTypography-noWrap")
         .should("have.length", textColumns.length);
     });
 
     it("Displays correct content in first row", () => {
-      cy.get("tbody > tr")
-        .first()
-        .find("td")
+      cy.findAllByRole("row")
+        .eq(1) // first row is headers, so get next index
         .each(($el, index) => {
           if (index === 0) {
             const iconTitle = firstRowData[index];
-            cy.wrap($el).find(`svg[aria-label=${iconTitle}]`);
+            cy.wrap($el).findByLabelText(iconTitle).should("exist");
           } else {
             cy.wrap($el)
               .find("p")
diff --git a/src/hooks/pagination.js b/src/hooks/pagination.js
new file mode 100644
index 00000000..3c37e95b
--- /dev/null
+++ b/src/hooks/pagination.js
@@ -0,0 +1,57 @@
+import { useCallback, useMemo, useState } from "react";
+
+const isZeroOrPositiveInt = (val) => {
+  return Number.isInteger(val) && val >= 0;
+};
+
+/**
+ * Manages pagination state e.g. outside a table or url.
+ *
+ * Provides getting/setting for a single pagination object, but internally
+ * manages changes for each attribute independently so that e.g. if there are
+ * no changes then the object is unchanged.
+ */
+export const usePagination = ({
+  initPage,
+  initLimit,
+  initTotalCount,
+  rowsPerPageOptions
+}) => {
+  const [page, setPage] = useState(initPage ?? 0);
+  const [_rowsPerPageOptions] = useState(rowsPerPageOptions ?? [20, 50]);
+  const [limit, setLimit] = useState(initLimit ?? _rowsPerPageOptions[0]);
+  const [totalCount, setTotalCount] = useState(initTotalCount ?? 0);
+
+  /**
+   * Returns current set of pagination values
+   */
+  const pagination = useMemo(() => {
+    const val = {
+      page,
+      limit,
+      rows: limit,
+      totalCount,
+      totalRecords: totalCount,
+      rowsPerPageOptions: _rowsPerPageOptions
+    };
+    return val;
+  }, [page, limit, totalCount, _rowsPerPageOptions]);
+
+  /**
+   * Sets pagination. Ignores negative or nonzero values.
+   */
+  const setPagination = useCallback(
+    ({ page, limit, totalCount, first, rows, totalRecords }) => {
+      isZeroOrPositiveInt(page) && setPage(page);
+      isZeroOrPositiveInt(limit || rows) && setLimit(limit || rows);
+      isZeroOrPositiveInt(totalCount || totalRecords) &&
+        setTotalCount(totalCount || totalRecords);
+    },
+    []
+  );
+
+  return {
+    pagination,
+    setPagination
+  };
+};
diff --git a/src/views/host/HostListView.js b/src/views/host/HostListView.js
index 5571dd31..5ee17904 100644
--- a/src/views/host/HostListView.js
+++ b/src/views/host/HostListView.js
@@ -1,4 +1,10 @@
-import React, { useState, useEffect, useCallback, useContext } from "react";
+import React, {
+  useState,
+  useEffect,
+  useCallback,
+  useContext,
+  useMemo
+} from "react";
 import { styled } from "@mui/material/styles";
 import {
   Container,
@@ -28,6 +34,7 @@ import {
   serialize,
   deserialize
 } from "../../components/common/URLState/URLState";
+import { usePagination } from "../../hooks/pagination";
 
 const PREFIX = "HostListView";
 
@@ -48,7 +55,7 @@ export function HostListView() {
   useEffect(() => setTitle(applicationTitle("IOC hosts")), [setTitle]);
 
   const [hosts, getHosts /* reset*/, , loading] = useCSEntrySearch();
-  const [state, setState] = useUrlState(
+  const [urlState, setUrlState] = useUrlState(
     {
       tab: "0",
       own: "false",
@@ -58,14 +65,31 @@ export function HostListView() {
     },
     { navigateMode: "replace" }
   );
-
   const [hostFilter, setHostFilter] = useState("ALL");
 
-  const hideOwnSlider = deserialize(state.tab) === 2;
+  const urlPagination = useMemo(
+    () => ({
+      rows: deserialize(urlState.rows),
+      page: deserialize(urlState.page)
+    }),
+    [urlState.rows, urlState.page]
+  );
+
+  const setUrlPagination = useCallback(
+    ({ rows, page }) => {
+      setUrlState({
+        rows: serialize(rows),
+        page: serialize(page)
+      });
+    },
+    [setUrlState]
+  );
+
+  const hideOwnSlider = deserialize(urlState.tab) === 2;
 
   const handleTabChange = useCallback(
     (event, tab) => {
-      setState((s) =>
+      setUrlState((s) =>
         serialize(s.tab) === serialize(tab)
           ? { tab: serialize(tab) }
           : { tab: serialize(tab), page: "0" }
@@ -73,7 +97,7 @@ export function HostListView() {
 
       changeTab(tab);
     },
-    [setState]
+    [setUrlState]
   );
 
   const changeTab = (tab) => {
@@ -87,59 +111,75 @@ export function HostListView() {
   };
 
   useEffect(() => {
-    state.tab && changeTab(deserialize(state.tab));
-  }, [state]);
+    urlState.tab && changeTab(deserialize(urlState.tab));
+  }, [urlState]);
 
   const handleChangeOwn = useCallback(
     (event) => {
       const own = event.target.checked;
-      setState({ own: serialize(own), page: "0" });
+      setUrlState({ own: serialize(own), page: "0" });
     },
-    [setState]
+    [setUrlState]
   );
 
-  const lazyParams = useCallback(() => {
-    return {
-      rows: deserialize(state.rows),
-      page: deserialize(state.page)
-    };
-  }, [state]);
+  const rowsPerPage = [20, 50];
 
-  const setLazyParams = useCallback(
-    (params) => {
-      setState({ rows: serialize(params.rows), page: serialize(params.page) });
-    },
-    [setState]
-  );
+  const { pagination, setPagination } = usePagination({
+    rowsPerPageOptions: rowsPerPage,
+    initLimit: urlPagination.rows ?? rowsPerPage[0],
+    initPage: urlPagination.page ?? 0
+  });
 
-  const rowsPerPage = [20, 50];
+  // update pagination whenever search result total pages change
+  useEffect(() => {
+    setPagination({ totalCount: hosts.totalCount ?? 0 });
+  }, [setPagination, hosts.totalCount]);
 
+  // whenever url state changes, update pagination
+  useEffect(() => {
+    setPagination({ ...urlPagination });
+  }, [setPagination, urlPagination]);
+
+  // whenever table pagination internally changes (user clicks next page etc)
+  // update the row params
+  useEffect(() => {
+    setUrlPagination(pagination);
+  }, [pagination, setUrlPagination]);
+
+  // Request new search results whenever search or pagination changes
   useEffect(() => {
     let requestParams = initRequestParams(
-      lazyParams(),
-      transformHostQuery(deserialize(state.query))
+      pagination,
+      transformHostQuery(deserialize(urlState.query))
     );
     requestParams.filter =
-      hostFilter !== "NO_IOCS" && deserialize(state.own) ? "OWN" : hostFilter;
-
+      hostFilter !== "NO_IOCS" && deserialize(urlState.own)
+        ? "OWN"
+        : hostFilter;
     getHosts(requestParams);
-  }, [getHosts, lazyParams, hostFilter, state]);
+  }, [getHosts, hostFilter, urlState.query, urlState.own, pagination]);
 
+  // Callback for searchbar, called whenever user updates search
   const setSearch = useCallback(
     (query) => {
-      setState({ query: serialize(query) });
+      setUrlState({ query: serialize(query) });
     },
-    [setState]
+    [setUrlState]
   );
 
   const smUp = useMediaQuery((theme) => theme.breakpoints.up("sm"));
   const smDown = useMediaQuery((theme) => theme.breakpoints.down("sm"));
 
+  // Invoked by Table on change to pagination
+  const onPage = (params) => {
+    setPagination(params);
+  };
+
   const content = (
     <>
       <SearchBar
         search={setSearch}
-        query={deserialize(state.query)}
+        query={deserialize(urlState.query)}
         loading={loading}
       >
         {smDown ? <HostList hosts={hosts.hostList} /> : null}
@@ -147,10 +187,8 @@ export function HostListView() {
           <HostTable
             hosts={hosts.hostList}
             loading={loading}
-            totalCount={hosts.totalCount}
-            lazyParams={lazyParams()}
-            setLazyParams={setLazyParams}
-            rowsPerPage={rowsPerPage}
+            pagination={pagination}
+            onPage={onPage}
           />
         ) : null}
       </SearchBar>
@@ -174,7 +212,7 @@ export function HostListView() {
           >
             <Grid item>
               <Tabs
-                value={deserialize(state.tab)}
+                value={deserialize(urlState.tab)}
                 onChange={handleTabChange}
               >
                 <Tab label={<Typography variant="h5">All</Typography>} />
@@ -206,7 +244,7 @@ export function HostListView() {
                       className={classes.formControl}
                       control={
                         <Switch
-                          checked={deserialize(state.own)}
+                          checked={deserialize(urlState.own)}
                           onChange={handleChangeOwn}
                         />
                       }
-- 
GitLab