foris 6.3.0 → 6.4.0-beta.087ecfa6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  * See /LICENSE for more information.
6
6
  */
7
7
 
8
- import React, { useRef } from "react";
8
+ import React, { useRef, useEffect, useState } from "react";
9
9
 
10
10
  import PropTypes from "prop-types";
11
11
 
@@ -40,20 +40,36 @@ Alert.defaultProps = {
40
40
 
41
41
  function Alert({ type, onDismiss, children }) {
42
42
  const alertRef = useRef();
43
+ const [isVisible, setIsVisible] = useState(true);
43
44
  useFocusTrap(alertRef, !!onDismiss);
45
+
46
+ useEffect(() => {
47
+ if (onDismiss) {
48
+ const timeout = setTimeout(() => setIsVisible(false), 7000);
49
+ return () => clearTimeout(timeout);
50
+ }
51
+ }, [onDismiss]);
52
+
53
+ const handleAnimationEnd = () => {
54
+ if (!isVisible && onDismiss) {
55
+ onDismiss();
56
+ }
57
+ };
58
+
44
59
  return (
45
60
  <div
46
61
  ref={alertRef}
47
- className={`alert alert-${type} ${
62
+ className={`alert alert-${type} ${isVisible ? "alert-fade-in" : "alert-slide-out-top"} ${
48
63
  onDismiss ? "alert-dismissible" : ""
49
64
  }`.trim()}
50
65
  role="alert"
66
+ onAnimationEnd={handleAnimationEnd}
51
67
  >
52
68
  {onDismiss && (
53
69
  <button
54
70
  type="button"
55
71
  className="btn-close"
56
- onClick={onDismiss}
72
+ onClick={() => setIsVisible(false)}
57
73
  aria-label={_("Close")}
58
74
  />
59
75
  )}
@@ -18,9 +18,9 @@ ThreeDotsMenu.propTypes = {
18
18
  children: PropTypes.arrayOf(PropTypes.node).isRequired,
19
19
  };
20
20
 
21
- function ThreeDotsMenu({ children }) {
21
+ function ThreeDotsMenu({ children, ...props }) {
22
22
  return (
23
- <div className="dropdown">
23
+ <div className="dropdown position-static" {...props}>
24
24
  <Button
25
25
  className="btn-sm btn-link text-body"
26
26
  data-bs-toggle="dropdown"
@@ -16,6 +16,11 @@ import { Modal, ModalHeader, ModalBody, ModalFooter } from "../bootstrap/Modal";
16
16
  import { useAlert } from "../context/alertContext/AlertContext";
17
17
  import { ForisURLs } from "../utils/forisUrls";
18
18
 
19
+ RebootButton.propTypes = {
20
+ /** Additional props to be passed to the button */
21
+ props: PropTypes.object,
22
+ };
23
+
19
24
  function RebootButton(props) {
20
25
  const [triggered, setTriggered] = useState(false);
21
26
  const [modalShown, setModalShown] = useState(false);
@@ -68,7 +73,12 @@ function RebootModal({ shown, setShown, onReboot }) {
68
73
  <p>{_("Are you sure you want to restart the router?")}</p>
69
74
  </ModalBody>
70
75
  <ModalFooter>
71
- <Button onClick={() => setShown(false)}>{_("Cancel")}</Button>
76
+ <Button
77
+ className="btn-secondary"
78
+ onClick={() => setShown(false)}
79
+ >
80
+ {_("Cancel")}
81
+ </Button>
72
82
  <Button className="btn-danger" onClick={onReboot}>
73
83
  {_("Confirm reboot")}
74
84
  </Button>
@@ -0,0 +1,24 @@
1
+ RebootButton component is a button that opens a modal dialog to confirm the
2
+ reboot of the device.
3
+
4
+ ## Usage
5
+
6
+ ```jsx
7
+ import React, { useEffect, createContext } from "react";
8
+ import RebootButton from "./RebootButton";
9
+ import { AlertContextProvider } from "../context/alertContext/AlertContext";
10
+
11
+ window.AlertContext = React.createContext();
12
+
13
+ const RebootButtonExample = () => {
14
+ return (
15
+ <AlertContextProvider>
16
+ <div id="modal-container" />
17
+ <div id="alert-container" />
18
+ <RebootButton />
19
+ </AlertContextProvider>
20
+ );
21
+ };
22
+
23
+ <RebootButtonExample />;
24
+ ```
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
3
+ *
4
+ * This is free software, licensed under the GNU General Public License v3.
5
+ * See /LICENSE for more information.
6
+ */
7
+
8
+ import React, { useMemo, useState } from "react";
9
+
10
+ import {
11
+ flexRender,
12
+ getCoreRowModel,
13
+ getSortedRowModel,
14
+ getPaginationRowModel,
15
+ useReactTable,
16
+ } from "@tanstack/react-table";
17
+ import PropTypes from "prop-types";
18
+
19
+ import RichTableBody from "./RichTableBody";
20
+ import RichTableHeader from "./RichTableHeader";
21
+ import RichTablePagination from "./RichTablePagination";
22
+
23
+ const fallbackData = [];
24
+
25
+ RichTable.propTypes = {
26
+ /** Columns to be displayed in the table */
27
+ columns: PropTypes.array.isRequired,
28
+ /** Data to be displayed in the table */
29
+ data: PropTypes.array.isRequired,
30
+ /** Whether to display pagination */
31
+ withPagination: PropTypes.bool,
32
+ /** Number of rows per page */
33
+ pageSize: PropTypes.number,
34
+ /** Index of the current page */
35
+ pageIndex: PropTypes.number,
36
+ };
37
+
38
+ function RichTable({
39
+ columns,
40
+ data,
41
+ withPagination,
42
+ pageSize = 5,
43
+ pageIndex = 0,
44
+ }) {
45
+ const tableColumns = useMemo(() => columns, [columns]);
46
+ const [tableData] = useState(data ?? fallbackData);
47
+ const [sorting, setSorting] = useState([]);
48
+ const [pagination, setPagination] = useState({
49
+ pageIndex,
50
+ pageSize,
51
+ });
52
+
53
+ const table = useReactTable({
54
+ data: tableData,
55
+ columns: tableColumns,
56
+ getCoreRowModel: getCoreRowModel(),
57
+ getSortedRowModel: getSortedRowModel(),
58
+ getPaginationRowModel: getPaginationRowModel(),
59
+ onPaginationChange: setPagination,
60
+ onSortingChange: setSorting,
61
+ state: {
62
+ sorting,
63
+ pagination,
64
+ },
65
+ });
66
+
67
+ return (
68
+ <div className="table-responsive">
69
+ <table className="table table-hover text-nowrap">
70
+ <RichTableHeader table={table} flexRender={flexRender} />
71
+ <RichTableBody table={table} flexRender={flexRender} />
72
+ </table>
73
+ {withPagination && (
74
+ <RichTablePagination
75
+ table={table}
76
+ tablePageSize={pageSize}
77
+ allRows={tableData.length}
78
+ />
79
+ )}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export default RichTable;
@@ -0,0 +1,135 @@
1
+ ### Description
2
+
3
+ Rich Table is a table component based on
4
+ [Tanstack React Table](https://tanstack.com/table/). It adds some features to
5
+ the table component, such as:
6
+
7
+ - **Pagination**: The table can be paginated.
8
+ - **Sorting**: The table can be sorted by columns.
9
+ - **Row Expansion**: The table rows can be expanded. (To be implemented)
10
+
11
+ ### Example
12
+
13
+ ```js
14
+ import RichTable from "./RichTable";
15
+
16
+ const columns = [
17
+ {
18
+ header: "Name",
19
+ accessorKey: "name",
20
+ },
21
+ {
22
+ header: "Surname",
23
+ accessorKey: "surname",
24
+ },
25
+ {
26
+ header: "Age",
27
+ accessorKey: "age",
28
+ },
29
+ {
30
+ header: "Phone",
31
+ accessorKey: "phone",
32
+ },
33
+ ];
34
+
35
+ const data = [
36
+ {
37
+ name: "John",
38
+ surname: "Coltrane",
39
+ age: 30,
40
+ phone: "123456789",
41
+ },
42
+ {
43
+ name: "Jane",
44
+ surname: "Doe",
45
+ age: 25,
46
+ phone: "987654321",
47
+ },
48
+ {
49
+ name: "Alice",
50
+ surname: "Smith",
51
+ age: 35,
52
+ phone: "123456789",
53
+ },
54
+ {
55
+ name: "Bob",
56
+ surname: "Smith",
57
+ age: 40,
58
+ phone: "987654321",
59
+ },
60
+ {
61
+ name: "Charlie",
62
+ surname: "Brown",
63
+ age: 45,
64
+ phone: "123456789",
65
+ },
66
+ {
67
+ name: "Daisy",
68
+ surname: "Brown",
69
+ age: 50,
70
+ phone: "987654321",
71
+ },
72
+ {
73
+ name: "Eve",
74
+ surname: "Johnson",
75
+ age: 55,
76
+ phone: "123456789",
77
+ },
78
+ {
79
+ name: "Frank",
80
+ surname: "Johnson",
81
+ age: 60,
82
+ phone: "987654321",
83
+ },
84
+ {
85
+ name: "Grace",
86
+ surname: "Williams",
87
+ age: 65,
88
+ phone: "123456789",
89
+ },
90
+ {
91
+ name: "Henry",
92
+ surname: "Williams",
93
+ age: 70,
94
+ phone: "987654321",
95
+ },
96
+ {
97
+ name: "Ivy",
98
+ surname: "Brown",
99
+ age: 75,
100
+ phone: "123456789",
101
+ },
102
+ {
103
+ name: "Jack",
104
+ surname: "Brown",
105
+ age: 80,
106
+ phone: "987654321",
107
+ },
108
+ {
109
+ name: "Kelly",
110
+ surname: "Johnson",
111
+ age: 85,
112
+ phone: "123456789",
113
+ },
114
+ {
115
+ name: "Liam",
116
+ surname: "Johnson",
117
+ age: 90,
118
+ phone: "987654321",
119
+ },
120
+ {
121
+ name: "Mia",
122
+ surname: "Williams",
123
+ age: 95,
124
+ phone: "123456789",
125
+ },
126
+ {
127
+ name: "Nathan",
128
+ surname: "Williams",
129
+ age: 100,
130
+ phone: "987654321",
131
+ },
132
+ ];
133
+
134
+ <RichTable columns={columns} data={data} withPagination />;
135
+ ```
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
3
+ *
4
+ * This is free software, licensed under the GNU General Public License v3.
5
+ * See /LICENSE for more information.
6
+ */
7
+
8
+ import React from "react";
9
+
10
+ import propTypes from "prop-types";
11
+
12
+ RichTableBody.propTypes = {
13
+ table: propTypes.shape({
14
+ getRowModel: propTypes.func.isRequired,
15
+ }).isRequired,
16
+ flexRender: propTypes.func.isRequired,
17
+ };
18
+
19
+ function RichTableBody({ table, flexRender }) {
20
+ return (
21
+ <tbody>
22
+ {table.getRowModel().rows.map((row) => {
23
+ return (
24
+ <tr key={row.id} className="align-middle">
25
+ {row.getVisibleCells().map((cell) => {
26
+ return (
27
+ <td
28
+ key={cell.id}
29
+ {...(cell.column.columnDef.className && {
30
+ className:
31
+ cell.column.columnDef.className,
32
+ })}
33
+ >
34
+ {flexRender(
35
+ cell.column.columnDef.cell,
36
+ cell.getContext()
37
+ )}
38
+ </td>
39
+ );
40
+ })}
41
+ </tr>
42
+ );
43
+ })}
44
+ </tbody>
45
+ );
46
+ }
47
+
48
+ export default RichTableBody;
@@ -0,0 +1,96 @@
1
+ /*
2
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
3
+ *
4
+ * This is free software, licensed under the GNU General Public License v3.
5
+ * See /LICENSE for more information.
6
+ */
7
+
8
+ import React from "react";
9
+
10
+ import {
11
+ faSquareCaretUp,
12
+ faSquareCaretDown,
13
+ } from "@fortawesome/free-solid-svg-icons";
14
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
15
+ import propTypes from "prop-types";
16
+
17
+ RichTableHeader.propTypes = {
18
+ table: propTypes.shape({
19
+ getHeaderGroups: propTypes.func.isRequired,
20
+ }).isRequired,
21
+ flexRender: propTypes.func.isRequired,
22
+ };
23
+
24
+ function RichTableHeader({ table, flexRender }) {
25
+ const getThTitle = (header) => {
26
+ if (!header.column.getCanSort()) return undefined;
27
+
28
+ const nextSortingOrder = header.column.getNextSortingOrder();
29
+ if (nextSortingOrder === "asc") return _("Sort ascending");
30
+ if (nextSortingOrder === "desc") return _("Sort descending");
31
+ return _("Clear sort");
32
+ };
33
+
34
+ return (
35
+ <thead className="thead-light">
36
+ {table.getHeaderGroups().map((headerGroup) => (
37
+ <tr key={headerGroup.id} role="row">
38
+ {headerGroup.headers.map((header) => (
39
+ <th
40
+ key={header.id}
41
+ colSpan={header.colSpan}
42
+ {...(header.column.columnDef.headerClassName && {
43
+ className:
44
+ header.column.columnDef.headerClassName,
45
+ })}
46
+ >
47
+ {header.isPlaceholder ||
48
+ header.column.columnDef.headerIsHidden ? (
49
+ <div className="d-none" aria-hidden="true">
50
+ {flexRender(
51
+ header.column.columnDef.header,
52
+ header.getContext()
53
+ )}
54
+ </div>
55
+ ) : (
56
+ <button
57
+ type="button"
58
+ className={`btn btn-link text-decoration-none text-reset fw-bold p-0 d-flex align-items-center
59
+ ${
60
+ header.column.getCanSort()
61
+ ? "d-flex align-items-center"
62
+ : ""
63
+ }
64
+ `}
65
+ onClick={header.column.getToggleSortingHandler()}
66
+ title={getThTitle(header)}
67
+ >
68
+ {flexRender(
69
+ header.column.columnDef.header,
70
+ header.getContext()
71
+ )}
72
+ {{
73
+ asc: (
74
+ <FontAwesomeIcon
75
+ icon={faSquareCaretUp}
76
+ className="ms-1 text-primary"
77
+ />
78
+ ),
79
+ desc: (
80
+ <FontAwesomeIcon
81
+ icon={faSquareCaretDown}
82
+ className="ms-1 text-primary"
83
+ />
84
+ ),
85
+ }[header.column.getIsSorted()] ?? null}
86
+ </button>
87
+ )}
88
+ </th>
89
+ ))}
90
+ </tr>
91
+ ))}
92
+ </thead>
93
+ );
94
+ }
95
+
96
+ export default RichTableHeader;
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
3
+ *
4
+ * This is free software, licensed under the GNU General Public License v3.
5
+ * See /LICENSE for more information.
6
+ */
7
+
8
+ import React, { useMemo } from "react";
9
+
10
+ import {
11
+ faAngleLeft,
12
+ faAnglesLeft,
13
+ faAngleRight,
14
+ faAnglesRight,
15
+ } from "@fortawesome/free-solid-svg-icons";
16
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
17
+ import propTypes from "prop-types";
18
+
19
+ RichTablePagination.propTypes = {
20
+ table: propTypes.shape({
21
+ getState: propTypes.func.isRequired,
22
+ getCanPreviousPage: propTypes.func.isRequired,
23
+ getCanNextPage: propTypes.func.isRequired,
24
+ firstPage: propTypes.func.isRequired,
25
+ previousPage: propTypes.func.isRequired,
26
+ nextPage: propTypes.func.isRequired,
27
+ lastPage: propTypes.func.isRequired,
28
+ setPageSize: propTypes.func.isRequired,
29
+ getPageCount: propTypes.func.isRequired,
30
+ }).isRequired,
31
+ tablePageSize: propTypes.number,
32
+ allRows: propTypes.number,
33
+ };
34
+
35
+ function RichTablePagination({ table, tablePageSize, allRows }) {
36
+ const { pagination } = table.getState();
37
+ const prevPagBtnDisabled = !table.getCanPreviousPage();
38
+ const nextPagBtnDisabled = !table.getCanNextPage();
39
+
40
+ const pageSizes = useMemo(() => {
41
+ return [tablePageSize ?? 5, 10, 25].filter(
42
+ (value, index, self) => self.indexOf(value) === index
43
+ );
44
+ }, [tablePageSize]);
45
+
46
+ const renderPaginationButton = (icon, ariaLabel, onClick, disabled) => (
47
+ <li
48
+ className={`page-item ${disabled ? "disabled" : ""}`}
49
+ style={{ cursor: disabled ? "not-allowed" : "pointer" }}
50
+ >
51
+ <button
52
+ type="button"
53
+ className="page-link"
54
+ aria-label={ariaLabel}
55
+ onClick={onClick}
56
+ disabled={disabled}
57
+ >
58
+ <FontAwesomeIcon icon={icon} />
59
+ </button>
60
+ </li>
61
+ );
62
+
63
+ return (
64
+ <nav
65
+ aria-label={_("Pagination navigation bar")}
66
+ className="d-flex gap-2 justify-content-start align-items-center mx-2 mb-1 text-nowrap"
67
+ >
68
+ <ul className="pagination pagination-sm mb-0">
69
+ {renderPaginationButton(
70
+ faAnglesLeft,
71
+ _("First page"),
72
+ () => table.firstPage(),
73
+ prevPagBtnDisabled
74
+ )}
75
+ {renderPaginationButton(
76
+ faAngleLeft,
77
+ _("Previous page"),
78
+ () => table.previousPage(),
79
+ prevPagBtnDisabled
80
+ )}
81
+ {renderPaginationButton(
82
+ faAngleRight,
83
+ _("Next page"),
84
+ () => table.nextPage(),
85
+ nextPagBtnDisabled
86
+ )}
87
+ {renderPaginationButton(
88
+ faAnglesRight,
89
+ _("Last page"),
90
+ () => table.lastPage(),
91
+ nextPagBtnDisabled
92
+ )}
93
+ </ul>
94
+ <span>
95
+ {_("Page")}&nbsp;
96
+ <span className="fw-bold">
97
+ {pagination.pageIndex + 1}
98
+ &nbsp;{_("of")}&nbsp;
99
+ {table.getPageCount().toLocaleString()}
100
+ </span>
101
+ </span>
102
+ <div
103
+ className="vr mx-1 align-self-center"
104
+ style={{ height: "1.5rem" }}
105
+ />
106
+ <span>{_("Rows per page:")}</span>
107
+ <select
108
+ className="form-select form-select-sm w-auto"
109
+ aria-label={_("Select rows per page")}
110
+ value={pagination.pageSize}
111
+ onChange={(e) => {
112
+ table.setPageSize(Number(e.target.value));
113
+ }}
114
+ >
115
+ {pageSizes.map((pageSize) => (
116
+ <option key={pageSize} value={pageSize}>
117
+ {pageSize}
118
+ </option>
119
+ ))}
120
+ <option key={allRows} value={allRows}>
121
+ {_("All")}
122
+ </option>
123
+ </select>
124
+ </nav>
125
+ );
126
+ }
127
+
128
+ export default RichTablePagination;
Binary file
package/index.js CHANGED
@@ -43,6 +43,7 @@ export { Modal, ModalBody, ModalFooter, ModalHeader } from "./bootstrap/Modal";
43
43
  export { default as RebootButton } from "./common/RebootButton";
44
44
  export { default as WiFiSettings } from "./common/WiFiSettings/WiFiSettings";
45
45
  export { default as ResetWiFiSettings } from "./common/WiFiSettings/ResetWiFiSettings";
46
+ export { default as RichTable } from "./common/RichTable/RichTable";
46
47
  // Form
47
48
  export { default as ForisForm } from "./form/components/ForisForm";
48
49
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foris",
3
- "version": "6.3.0",
3
+ "version": "6.4.0-beta.087ecfa6",
4
4
  "description": "Foris JS library is a set of components and utils for reForis application and plugins.",
5
5
  "author": "CZ.NIC, z.s.p.o.",
6
6
  "repository": {
@@ -18,6 +18,7 @@
18
18
  "@fortawesome/free-regular-svg-icons": "^6.6.0",
19
19
  "@fortawesome/free-solid-svg-icons": "^6.6.0",
20
20
  "@fortawesome/react-fontawesome": "^0.2.2",
21
+ "@tanstack/react-table": "^8.20.5",
21
22
  "axios": "^1.7.2",
22
23
  "immutability-helper": "^3.1.1",
23
24
  "moment": "^2.30.1",
@@ -14,7 +14,7 @@ import { render } from "@testing-library/react";
14
14
  import PropTypes from "prop-types";
15
15
 
16
16
  import { AlertContextMock } from "./alertContextMock";
17
- import { CustomizationContextMock } from "./cutomizationContextMock";
17
+ import { CustomizationContextMock } from "./customizationContextMock";
18
18
 
19
19
  Wrapper.propTypes = {
20
20
  children: PropTypes.oneOfType([
package/foris-6.3.0.tgz DELETED
Binary file