diff --git a/package-lock.json b/package-lock.json
index 3cb34d6..92e1fe8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
+ "@tanstack/react-table": "^8.20.5",
"axios": "^1.7.2",
"immutability-helper": "^3.1.1",
"moment": "^2.30.1",
@@ -3583,6 +3584,39 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@tanstack/react-table": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
+ "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/table-core": "8.20.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@tanstack/table-core": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
+ "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-5.6.1.tgz",
@@ -15493,7 +15527,6 @@
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz",
"integrity": "sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -16247,7 +16280,6 @@
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
"integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@@ -21102,6 +21134,19 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "@tanstack/react-table": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
+ "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
+ "requires": {
+ "@tanstack/table-core": "8.20.5"
+ }
+ },
+ "@tanstack/table-core": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
+ "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg=="
+ },
"@testing-library/dom": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-5.6.1.tgz",
@@ -30083,7 +30128,6 @@
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz",
"integrity": "sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==",
- "dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -30648,7 +30692,6 @@
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
"integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==",
- "dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
diff --git a/package.json b/package.json
index 7fa058e..3599f3e 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
+ "@tanstack/react-table": "^8.20.5",
"axios": "^1.7.2",
"immutability-helper": "^3.1.1",
"moment": "^2.30.1",
diff --git a/src/common/RebootButton.js b/src/common/RebootButton.js
index 9b9f85e..380b30d 100644
--- a/src/common/RebootButton.js
+++ b/src/common/RebootButton.js
@@ -16,6 +16,11 @@ import { Modal, ModalHeader, ModalBody, ModalFooter } from "../bootstrap/Modal";
import { useAlert } from "../context/alertContext/AlertContext";
import { ForisURLs } from "../utils/forisUrls";
+RebootButton.propTypes = {
+ /** Additional props to be passed to the button */
+ props: PropTypes.object,
+};
+
function RebootButton(props) {
const [triggered, setTriggered] = useState(false);
const [modalShown, setModalShown] = useState(false);
@@ -68,7 +73,12 @@ function RebootModal({ shown, setShown, onReboot }) {
{_("Are you sure you want to restart the router?")}
-
+
diff --git a/src/common/RebootButton.md b/src/common/RebootButton.md
new file mode 100644
index 0000000..58a328a
--- /dev/null
+++ b/src/common/RebootButton.md
@@ -0,0 +1,24 @@
+RebootButton component is a button that opens a modal dialog to confirm the
+reboot of the device.
+
+## Usage
+
+```jsx
+import React, { useEffect, createContext } from "react";
+import RebootButton from "./RebootButton";
+import { AlertContextProvider } from "../context/alertContext/AlertContext";
+
+window.AlertContext = React.createContext();
+
+const RebootButtonExample = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+;
+```
diff --git a/src/common/RichTable/RichTable.js b/src/common/RichTable/RichTable.js
new file mode 100644
index 0000000..98e42eb
--- /dev/null
+++ b/src/common/RichTable/RichTable.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
+ *
+ * This is free software, licensed under the GNU General Public License v3.
+ * See /LICENSE for more information.
+ */
+
+import React, { useMemo, useState } from "react";
+
+import {
+ flexRender,
+ getCoreRowModel,
+ getSortedRowModel,
+ getPaginationRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import PropTypes from "prop-types";
+
+import RichTableBody from "./RichTableBody";
+import RichTableHeader from "./RichTableHeader";
+import RichTablePagination from "./RichTablePagination";
+
+const fallbackData = [];
+
+RichTable.propTypes = {
+ /** Columns to be displayed in the table */
+ columns: PropTypes.array.isRequired,
+ /** Data to be displayed in the table */
+ data: PropTypes.array.isRequired,
+ /** Whether to display pagination */
+ withPagination: PropTypes.bool,
+ /** Number of rows per page */
+ pageSize: PropTypes.number,
+ /** Index of the current page */
+ pageIndex: PropTypes.number,
+};
+
+function RichTable({
+ columns,
+ data,
+ withPagination,
+ pageSize = 5,
+ pageIndex = 0,
+}) {
+ const tableColumns = useMemo(() => columns, [columns]);
+ const [tableData] = useState(data ?? fallbackData);
+ const [sorting, setSorting] = useState([]);
+ const [pagination, setPagination] = useState({
+ pageIndex,
+ pageSize,
+ });
+
+ const table = useReactTable({
+ data: tableData,
+ columns: tableColumns,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onPaginationChange: setPagination,
+ onSortingChange: setSorting,
+ state: {
+ sorting,
+ pagination,
+ },
+ });
+
+ return (
+
+
+ {withPagination && (
+
+ )}
+
+ );
+}
+
+export default RichTable;
diff --git a/src/common/RichTable/RichTable.md b/src/common/RichTable/RichTable.md
new file mode 100644
index 0000000..16a496c
--- /dev/null
+++ b/src/common/RichTable/RichTable.md
@@ -0,0 +1,135 @@
+### Description
+
+Rich Table is a table component based on
+[Tanstack React Table](https://tanstack.com/table/). It adds some features to
+the table component, such as:
+
+- **Pagination**: The table can be paginated.
+- **Sorting**: The table can be sorted by columns.
+- **Row Expansion**: The table rows can be expanded. (To be implemented)
+
+### Example
+
+```js
+import RichTable from "./RichTable";
+
+const columns = [
+ {
+ header: "Name",
+ accessorKey: "name",
+ },
+ {
+ header: "Surname",
+ accessorKey: "surname",
+ },
+ {
+ header: "Age",
+ accessorKey: "age",
+ },
+ {
+ header: "Phone",
+ accessorKey: "phone",
+ },
+];
+
+const data = [
+ {
+ name: "John",
+ surname: "Coltrane",
+ age: 30,
+ phone: "123456789",
+ },
+ {
+ name: "Jane",
+ surname: "Doe",
+ age: 25,
+ phone: "987654321",
+ },
+ {
+ name: "Alice",
+ surname: "Smith",
+ age: 35,
+ phone: "123456789",
+ },
+ {
+ name: "Bob",
+ surname: "Smith",
+ age: 40,
+ phone: "987654321",
+ },
+ {
+ name: "Charlie",
+ surname: "Brown",
+ age: 45,
+ phone: "123456789",
+ },
+ {
+ name: "Daisy",
+ surname: "Brown",
+ age: 50,
+ phone: "987654321",
+ },
+ {
+ name: "Eve",
+ surname: "Johnson",
+ age: 55,
+ phone: "123456789",
+ },
+ {
+ name: "Frank",
+ surname: "Johnson",
+ age: 60,
+ phone: "987654321",
+ },
+ {
+ name: "Grace",
+ surname: "Williams",
+ age: 65,
+ phone: "123456789",
+ },
+ {
+ name: "Henry",
+ surname: "Williams",
+ age: 70,
+ phone: "987654321",
+ },
+ {
+ name: "Ivy",
+ surname: "Brown",
+ age: 75,
+ phone: "123456789",
+ },
+ {
+ name: "Jack",
+ surname: "Brown",
+ age: 80,
+ phone: "987654321",
+ },
+ {
+ name: "Kelly",
+ surname: "Johnson",
+ age: 85,
+ phone: "123456789",
+ },
+ {
+ name: "Liam",
+ surname: "Johnson",
+ age: 90,
+ phone: "987654321",
+ },
+ {
+ name: "Mia",
+ surname: "Williams",
+ age: 95,
+ phone: "123456789",
+ },
+ {
+ name: "Nathan",
+ surname: "Williams",
+ age: 100,
+ phone: "987654321",
+ },
+];
+
+;
+```
diff --git a/src/common/RichTable/RichTableBody.js b/src/common/RichTable/RichTableBody.js
new file mode 100644
index 0000000..2aca395
--- /dev/null
+++ b/src/common/RichTable/RichTableBody.js
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
+ *
+ * This is free software, licensed under the GNU General Public License v3.
+ * See /LICENSE for more information.
+ */
+
+import React from "react";
+
+import propTypes from "prop-types";
+
+RichTableBody.propTypes = {
+ table: propTypes.shape({
+ getRowModel: propTypes.func.isRequired,
+ }).isRequired,
+ flexRender: propTypes.func.isRequired,
+};
+
+function RichTableBody({ table, flexRender }) {
+ return (
+
+ {table.getRowModel().rows.map((row) => {
+ return (
+
+ {row.getVisibleCells().map((cell) => {
+ return (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ |
+ );
+ })}
+
+ );
+ })}
+
+ );
+}
+
+export default RichTableBody;
diff --git a/src/common/RichTable/RichTableHeader.js b/src/common/RichTable/RichTableHeader.js
new file mode 100644
index 0000000..79e6ca9
--- /dev/null
+++ b/src/common/RichTable/RichTableHeader.js
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
+ *
+ * This is free software, licensed under the GNU General Public License v3.
+ * See /LICENSE for more information.
+ */
+
+import React from "react";
+
+import {
+ faSquareCaretUp,
+ faSquareCaretDown,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import propTypes from "prop-types";
+
+RichTableHeader.propTypes = {
+ table: propTypes.shape({
+ getHeaderGroups: propTypes.func.isRequired,
+ }).isRequired,
+ flexRender: propTypes.func.isRequired,
+};
+
+function RichTableHeader({ table, flexRender }) {
+ const getThTitle = (header) => {
+ if (!header.column.getCanSort()) return undefined;
+
+ const nextSortingOrder = header.column.getNextSortingOrder();
+ if (nextSortingOrder === "asc") return _("Sort ascending");
+ if (nextSortingOrder === "desc") return _("Sort descending");
+ return _("Clear sort");
+ };
+
+ return (
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder ||
+ header.column.columnDef.headerIsHidden ? (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ ) : (
+
+ )}
+ |
+ ))}
+
+ ))}
+
+ );
+}
+
+export default RichTableHeader;
diff --git a/src/common/RichTable/RichTablePagination.js b/src/common/RichTable/RichTablePagination.js
new file mode 100644
index 0000000..c6821f6
--- /dev/null
+++ b/src/common/RichTable/RichTablePagination.js
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
+ *
+ * This is free software, licensed under the GNU General Public License v3.
+ * See /LICENSE for more information.
+ */
+
+import React, { useMemo } from "react";
+
+import {
+ faAngleLeft,
+ faAnglesLeft,
+ faAngleRight,
+ faAnglesRight,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import propTypes from "prop-types";
+
+RichTablePagination.propTypes = {
+ table: propTypes.shape({
+ getState: propTypes.func.isRequired,
+ getCanPreviousPage: propTypes.func.isRequired,
+ getCanNextPage: propTypes.func.isRequired,
+ firstPage: propTypes.func.isRequired,
+ previousPage: propTypes.func.isRequired,
+ nextPage: propTypes.func.isRequired,
+ lastPage: propTypes.func.isRequired,
+ setPageSize: propTypes.func.isRequired,
+ getPageCount: propTypes.func.isRequired,
+ }).isRequired,
+ tablePageSize: propTypes.number,
+ allRows: propTypes.number,
+};
+
+function RichTablePagination({ table, tablePageSize, allRows }) {
+ const { pagination } = table.getState();
+ const prevPagBtnDisabled = !table.getCanPreviousPage();
+ const nextPagBtnDisabled = !table.getCanNextPage();
+
+ const pageSizes = useMemo(() => {
+ return [tablePageSize ?? 5, 10, 25].filter(
+ (value, index, self) => self.indexOf(value) === index
+ );
+ }, [tablePageSize]);
+
+ const renderPaginationButton = (icon, ariaLabel, onClick, disabled) => (
+
+
+
+ );
+
+ return (
+
+ );
+}
+
+export default RichTablePagination;
diff --git a/src/common/__tests__/__snapshots__/RebootButton.test.js.snap b/src/common/__tests__/__snapshots__/RebootButton.test.js.snap
index 6ee8552..449587c 100644
--- a/src/common/__tests__/__snapshots__/RebootButton.test.js.snap
+++ b/src/common/__tests__/__snapshots__/RebootButton.test.js.snap
@@ -43,7 +43,7 @@ exports[` Render modal. 1`] = `
class="modal-footer"
>