mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
User list tab (#44)
The new user tab displays a table of users that belong to the account.
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"heroicons": "^1.0.6",
|
"heroicons": "^1.0.6",
|
||||||
"highlight.js": "^11.2.0",
|
"highlight.js": "^11.2.0",
|
||||||
"history": "^5.0.1",
|
"history": "^5.0.1",
|
||||||
|
"nth-check": ">=2.0.1",
|
||||||
"postcss": "^8.4.12",
|
"postcss": "^8.4.12",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"tailwindcss": "^3.0.23",
|
"tailwindcss": "^3.0.23",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4",
|
||||||
|
"nth-check": ">=2.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, {useEffect, useState} from 'react';
|
|||||||
import Navbar from './components/Navbar';
|
import Navbar from './components/Navbar';
|
||||||
import {Redirect, Route, Switch} from 'react-router-dom';
|
import {Redirect, Route, Switch} from 'react-router-dom';
|
||||||
import {Peers} from './views/Peers';
|
import {Peers} from './views/Peers';
|
||||||
|
import {Users} from './views/Users';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import {useAuth0} from "@auth0/auth0-react";
|
import {useAuth0} from "@auth0/auth0-react";
|
||||||
import Loading from "./components/Loading";
|
import Loading from "./components/Loading";
|
||||||
@@ -76,6 +77,7 @@ function App() {
|
|||||||
<Route path="/setup-keys" component={SetupKeys}/>
|
<Route path="/setup-keys" component={SetupKeys}/>
|
||||||
<Route path="/acls" component={AccessControl}/>
|
<Route path="/acls" component={AccessControl}/>
|
||||||
<Route path="/activity" component={Activity}/>
|
<Route path="/activity" component={Activity}/>
|
||||||
|
<Route path="/users" component={Users}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export const callApi = async (method, headers, body, getAccessTokenSilently, end
|
|||||||
return await response.json();
|
return await response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUsers = async (getAccessTokenSilently) => {
|
||||||
|
return callApi("GET", {}, null, getAccessTokenSilently, "/api/users")
|
||||||
|
}
|
||||||
|
|
||||||
export const getSetupKeys = async (getAccessTokenSilently) => {
|
export const getSetupKeys = async (getAccessTokenSilently) => {
|
||||||
return callApi("GET", {}, null, getAccessTokenSilently, "/api/setup-keys")
|
return callApi("GET", {}, null, getAccessTokenSilently, "/api/setup-keys")
|
||||||
}
|
}
|
||||||
@@ -64,4 +68,4 @@ export const deletePeer = async (getAccessTokenSilently, peerId) => {
|
|||||||
null,
|
null,
|
||||||
getAccessTokenSilently,
|
getAccessTokenSilently,
|
||||||
"/api/peers/" + peerId)
|
"/api/peers/" + peerId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,15 @@ const Navbar = ({toggle}) => {
|
|||||||
Activity
|
Activity
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
|
{isAuthenticated && (
|
||||||
|
<NavLink
|
||||||
|
to="/users"
|
||||||
|
activeClassName="border-indigo-500 text-gray-900 border-b-2"
|
||||||
|
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,6 +211,14 @@ const Navbar = ({toggle}) => {
|
|||||||
Activity
|
Activity
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
{isAuthenticated && (
|
||||||
|
<Link
|
||||||
|
to="/users"
|
||||||
|
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 pb-3 border-t border-gray-200">
|
<div className="pt-4 pb-3 border-t border-gray-200">
|
||||||
<div className="flex items-center px-4">
|
<div className="flex items-center px-4">
|
||||||
|
|||||||
@@ -13,247 +13,247 @@ import Loading from "../components/Loading";
|
|||||||
import { timeAgo } from "../utils/common";
|
import { timeAgo } from "../utils/common";
|
||||||
|
|
||||||
export const Peers = () => {
|
export const Peers = () => {
|
||||||
const [peers, setPeers] = useState([]);
|
const [peers, setPeers] = useState([]);
|
||||||
const [peersBackUp, setPeersBackUp] = useState([]);
|
const [peersBackUp, setPeersBackUp] = useState([]);
|
||||||
const [empty, setEmpty] = useState(true);
|
const [empty, setEmpty] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [peerToDelete, setPeerToDelete] = useState(null);
|
const [peerToDelete, setPeerToDelete] = useState(null);
|
||||||
const [deleteDialogText, setDeleteDialogText] = useState("");
|
const [deleteDialogText, setDeleteDialogText] = useState("");
|
||||||
const [deleteDialogTitle, setDeleteDialogTitle] = useState("");
|
const [deleteDialogTitle, setDeleteDialogTitle] = useState("");
|
||||||
|
|
||||||
const { getAccessTokenSilently } = useAuth0();
|
const { getAccessTokenSilently } = useAuth0();
|
||||||
|
|
||||||
const handleError = (error) => {
|
const handleError = (error) => {
|
||||||
console.error("Error to fetch data:", error);
|
console.error("Error to fetch data:", error);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(error);
|
setError(error);
|
||||||
};
|
};
|
||||||
// Add React Table
|
// Add React Table
|
||||||
const data = React.useMemo(() => peers, [peers]);
|
const data = React.useMemo(() => peers, [peers]);
|
||||||
|
|
||||||
const columns = React.useMemo(
|
const columns = React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
Header: "Name",
|
Header: "Name",
|
||||||
accessor: "Name",
|
accessor: "Name",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "IP",
|
Header: "IP",
|
||||||
accessor: "IP",
|
accessor: "IP",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Status",
|
Header: "Status",
|
||||||
accessor: "Connected",
|
accessor: "Connected",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Last Seen",
|
Header: "Last Seen",
|
||||||
accessor: "LastSeen",
|
accessor: "LastSeen",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "OS",
|
Header: "OS",
|
||||||
accessor: "OS",
|
accessor: "OS",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Version",
|
Header: "Version",
|
||||||
accessor: "Version",
|
accessor: "Version",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const td_class_name =
|
const td_class_name =
|
||||||
"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6";
|
"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6";
|
||||||
const td_class_other = "whitespace-nowrap px-3 py-4 text-sm text-gray-500";
|
const td_class_other = "whitespace-nowrap px-3 py-4 text-sm text-gray-500";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
headerGroups,
|
headerGroups,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
page,
|
page,
|
||||||
canPreviousPage,
|
canPreviousPage,
|
||||||
canNextPage,
|
canNextPage,
|
||||||
pageCount,
|
pageCount,
|
||||||
gotoPage,
|
gotoPage,
|
||||||
nextPage,
|
nextPage,
|
||||||
previousPage,
|
previousPage,
|
||||||
state: { pageIndex, pageSize },
|
state: { pageIndex, pageSize },
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{ columns, data, initialState: { pageIndex: 0, pageSize: 5 } },
|
{ columns, data, initialState: { pageIndex: 0, pageSize: 5 } },
|
||||||
usePagination
|
usePagination
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
const handleSearch = (e) => {
|
||||||
let tempArray = peersBackUp.filter((item) =>
|
let tempArray = peersBackUp.filter((item) => {
|
||||||
item.Name.toUpperCase().includes(e.toUpperCase())
|
return item.Name.toUpperCase().includes(e.toUpperCase()) || item.IP.toUpperCase().includes(e.toUpperCase())
|
||||||
);
|
});
|
||||||
setPeers(tempArray);
|
setPeers(tempArray);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortTable = (e) => {
|
const sortTable = (e) => {
|
||||||
let peerCopy = [...peers];
|
let peerCopy = [...peers];
|
||||||
if (e === "0") {
|
if (e === "0") {
|
||||||
peerCopy.sort((a, b) => (a.Name > b.Name ? 1 : -1));
|
peerCopy.sort((a, b) => (a.Name > b.Name ? 1 : -1));
|
||||||
} else if (e === "1") {
|
} else if (e === "1") {
|
||||||
peerCopy.sort((a, b) => (a.Name > b.Name ? -1 : 1));
|
peerCopy.sort((a, b) => (a.Name > b.Name ? -1 : 1));
|
||||||
} else if (e === "2") {
|
} else if (e === "2") {
|
||||||
peerCopy.sort((a, b) => (a.LastSeen > b.LastSeen ? 1 : -1));
|
peerCopy.sort((a, b) => (a.LastSeen > b.LastSeen ? 1 : -1));
|
||||||
} else if (e === "3") {
|
} else if (e === "3") {
|
||||||
peerCopy.sort((a, b) => (a.LastSeen > b.LastSeen ? -1 : 1));
|
peerCopy.sort((a, b) => (a.LastSeen > b.LastSeen ? -1 : 1));
|
||||||
} else {
|
} else {
|
||||||
console.log(`Sorry, we are out of ${e}`, e);
|
console.log(`Sorry, we are out of ${e}`, e);
|
||||||
}
|
}
|
||||||
setPeers(peerCopy);
|
setPeers(peerCopy);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InnerPageNumbers = () => {
|
const InnerPageNumbers = () => {
|
||||||
let default_btn =
|
let default_btn =
|
||||||
"z-10 bg-white border-gray-300 text-gray-700 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
|
"z-10 bg-white border-gray-300 text-gray-700 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
|
||||||
let clicked_btn =
|
let clicked_btn =
|
||||||
"z-10 bg-gray-50 border-gray-500 text-gray-600 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
|
"z-10 bg-gray-50 border-gray-500 text-gray-600 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
|
||||||
let menuItems = []
|
let menuItems = [];
|
||||||
if (pageCount < 6) {
|
if (pageCount < 6) {
|
||||||
for (let i = 0; i < pageCount; i++) {
|
for (let i = 0; i < pageCount; i++) {
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<button
|
<button
|
||||||
className={pageIndex === i ? clicked_btn : default_btn}
|
className={pageIndex === i ? clicked_btn : default_btn}
|
||||||
onClick={() => gotoPage(i)}
|
onClick={() => gotoPage(i)}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let j =
|
let j =
|
||||||
pageIndex === 0 || pageIndex === 1
|
pageIndex === 0 || pageIndex === 1
|
||||||
? 0
|
? 0
|
||||||
: pageCount - pageIndex === 1 ||
|
: pageCount - pageIndex === 1 ||
|
||||||
pageCount - pageIndex === 0 ||
|
pageCount - pageIndex === 0 ||
|
||||||
pageCount - pageIndex === 2
|
pageCount - pageIndex === 2
|
||||||
? pageCount - 5
|
? pageCount - 5
|
||||||
: pageIndex - 2;
|
: pageIndex - 2;
|
||||||
for (let i = j; i < j + 5; i++) {
|
for (let i = j; i < j + 5; i++) {
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<button
|
<button
|
||||||
className={pageIndex === i ? clicked_btn : default_btn}
|
className={pageIndex === i ? clicked_btn : default_btn}
|
||||||
onClick={() => gotoPage(i)}
|
onClick={() => gotoPage(i)}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <div>{menuItems}</div>;
|
return <div>{menuItems}</div>;
|
||||||
};
|
};
|
||||||
const formatOS = (os) => {
|
|
||||||
if (os.startsWith("windows 10")) {
|
|
||||||
return "Windows 10";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (os.startsWith("Darwin")) {
|
const formatOS = (os) => {
|
||||||
return os.replace("Darwin", "MacOS");
|
if (os.startsWith("windows 10")) {
|
||||||
}
|
return "Windows 10";
|
||||||
|
}
|
||||||
|
|
||||||
return os;
|
if (os.startsWith("Darwin")) {
|
||||||
};
|
return os.replace("Darwin", "MacOS");
|
||||||
|
}
|
||||||
|
|
||||||
//called when user clicks on table row menu item
|
return os;
|
||||||
const handleRowMenuClick = (action, peer) => {
|
};
|
||||||
if (action === "Delete") {
|
|
||||||
setPeerToDelete(peer[1].value);
|
|
||||||
setDeleteDialogText(
|
|
||||||
"Are you sure you want to delete peer from your account?"
|
|
||||||
);
|
|
||||||
setDeleteDialogTitle('Delete peer "' + peer[0].value + '"');
|
|
||||||
setShowDeleteDialog(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showAll = () => {
|
//called when user clicks on table row menu item
|
||||||
const showAllBtn = document.getElementById("btn-show-all");
|
const handleRowMenuClick = (action, peer) => {
|
||||||
const showOnlineBtn = document.getElementById("btn-show-online");
|
if (action === "Delete") {
|
||||||
|
setPeerToDelete(peer[1].value);
|
||||||
|
setDeleteDialogText(
|
||||||
|
"Are you sure you want to delete peer from your account?"
|
||||||
|
);
|
||||||
|
setDeleteDialogTitle('Delete peer "' + peer[0].value + '"');
|
||||||
|
setShowDeleteDialog(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
showAllBtn.classList.add(
|
const showAll = () => {
|
||||||
"ring-1",
|
const showAllBtn = document.getElementById("btn-show-all");
|
||||||
"ring-indigo-500",
|
const showOnlineBtn = document.getElementById("btn-show-online");
|
||||||
"border-indigo-500",
|
|
||||||
"outline-none"
|
|
||||||
);
|
|
||||||
showOnlineBtn.classList.remove(
|
|
||||||
"ring-1",
|
|
||||||
"ring-indigo-500",
|
|
||||||
"border-indigo-500",
|
|
||||||
"outline-none"
|
|
||||||
);
|
|
||||||
refresh(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showConnected = () => {
|
showAllBtn.classList.add(
|
||||||
const showAllBtn = document.getElementById("btn-show-all");
|
"ring-1",
|
||||||
const showOnlineBtn = document.getElementById("btn-show-online");
|
"ring-indigo-500",
|
||||||
|
"border-indigo-500",
|
||||||
|
"outline-none"
|
||||||
|
);
|
||||||
|
showOnlineBtn.classList.remove(
|
||||||
|
"ring-1",
|
||||||
|
"ring-indigo-500",
|
||||||
|
"border-indigo-500",
|
||||||
|
"outline-none"
|
||||||
|
);
|
||||||
|
refresh(null);
|
||||||
|
};
|
||||||
|
|
||||||
showOnlineBtn.classList.add(
|
const showConnected = () => {
|
||||||
"ring-1",
|
const showAllBtn = document.getElementById("btn-show-all");
|
||||||
"ring-indigo-500",
|
const showOnlineBtn = document.getElementById("btn-show-online");
|
||||||
"border-indigo-500",
|
|
||||||
"outline-none"
|
|
||||||
);
|
|
||||||
showAllBtn.classList.remove(
|
|
||||||
"ring-1",
|
|
||||||
"ring-indigo-500",
|
|
||||||
"border-indigo-500",
|
|
||||||
"outline-none"
|
|
||||||
);
|
|
||||||
|
|
||||||
refresh(function (peers) {
|
showOnlineBtn.classList.add(
|
||||||
return peers.filter((peer) => {
|
"ring-1",
|
||||||
return peer.Connected;
|
"ring-indigo-500",
|
||||||
});
|
"border-indigo-500",
|
||||||
});
|
"outline-none"
|
||||||
};
|
);
|
||||||
|
showAllBtn.classList.remove(
|
||||||
|
"ring-1",
|
||||||
|
"ring-indigo-500",
|
||||||
|
"border-indigo-500",
|
||||||
|
"outline-none"
|
||||||
|
);
|
||||||
|
|
||||||
const refresh = (filter) => {
|
refresh(function (peers) {
|
||||||
getPeers(getAccessTokenSilently)
|
return peers.filter((peer) => {
|
||||||
.then((responseData) =>
|
return peer.Connected;
|
||||||
responseData.sort((a, b) => (a.Name > b.Name ? 1 : -1))
|
});
|
||||||
)
|
});
|
||||||
.then((list) => {
|
};
|
||||||
setEmpty(list.length === 0);
|
|
||||||
return list;
|
|
||||||
})
|
|
||||||
.then((sorted) => {
|
|
||||||
return filter != null ? filter(sorted) : sorted;
|
|
||||||
})
|
|
||||||
.then((filtered) => {
|
|
||||||
setPeersBackUp(filtered);
|
|
||||||
setPeers(filtered);
|
|
||||||
})
|
|
||||||
.then(() => setLoading(false))
|
|
||||||
.catch((error) => handleError(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
// after user confirms (or not) deletion of the peer
|
const refresh = (filter) => {
|
||||||
const handleDeleteConfirmation = (confirmed) => {
|
getPeers(getAccessTokenSilently)
|
||||||
setShowDeleteDialog(false);
|
.then((responseData) =>
|
||||||
if (confirmed) {
|
responseData.sort((a, b) => (a.Name > b.Name ? 1 : -1))
|
||||||
deletePeer(getAccessTokenSilently, peerToDelete)
|
)
|
||||||
.then(() => setPeerToDelete(null))
|
.then((list) => {
|
||||||
.then(() => refresh(null))
|
setEmpty(list.length === 0);
|
||||||
.catch((error) => {
|
return list;
|
||||||
setPeerToDelete(null);
|
})
|
||||||
console.log(error);
|
.then((sorted) => {
|
||||||
});
|
return filter != null ? filter(sorted) : sorted;
|
||||||
} else {
|
})
|
||||||
setPeerToDelete(null);
|
.then((filtered) => {
|
||||||
}
|
setPeersBackUp(filtered);
|
||||||
};
|
setPeers(filtered);
|
||||||
|
})
|
||||||
|
.then(() => setLoading(false))
|
||||||
|
.catch((error) => handleError(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
// after user confirms (or not) deletion of the peer
|
||||||
|
const handleDeleteConfirmation = (confirmed) => {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
if (confirmed) {
|
||||||
|
deletePeer(getAccessTokenSilently, peerToDelete)
|
||||||
|
.then(() => setPeerToDelete(null))
|
||||||
|
.then(() => refresh(null))
|
||||||
|
.catch((error) => {
|
||||||
|
setPeerToDelete(null);
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPeerToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh(null);
|
refresh(null);
|
||||||
}, [getAccessTokenSilently]);
|
}, [getAccessTokenSilently]);
|
||||||
useEffect(() => {}, [peers]);
|
useEffect(() => {}, [peers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-10 bg-gray-50 overflow-hidden rounded max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div className="py-10 bg-gray-50 overflow-hidden rounded max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
@@ -544,6 +544,7 @@ export const Peers = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withAuthenticationRequired(Peers, {
|
export default withAuthenticationRequired(Peers, {
|
||||||
onRedirecting: () => <Loading />,
|
onRedirecting: () => <Loading />,
|
||||||
});
|
});
|
||||||
|
|||||||
467
src/views/Users.js
Normal file
467
src/views/Users.js
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { usePagination, useTable } from "react-table";
|
||||||
|
import { getUsers } from "../api/ManagementAPI";
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
||||||
|
import EmptyPeersPanel from "../components/EmptyPeers";
|
||||||
|
import Loading from "../components/Loading";
|
||||||
|
|
||||||
|
export const Users = () => {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [usersBackup, setUsersBackup] = useState([]);
|
||||||
|
const [empty, setEmpty] = useState(true);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const { getAccessTokenSilently } = useAuth0();
|
||||||
|
|
||||||
|
const handleError = (error) => {
|
||||||
|
console.error("Error to fetch data:", error);
|
||||||
|
setLoading(false);
|
||||||
|
setError(error);
|
||||||
|
};
|
||||||
|
// Add React Table
|
||||||
|
const data = React.useMemo(() => users, [users]);
|
||||||
|
|
||||||
|
const columns = React.useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: "Email",
|
||||||
|
accessor: "email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Name",
|
||||||
|
accessor: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Role",
|
||||||
|
accessor: "role",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const td_class_email =
|
||||||
|
"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6";
|
||||||
|
const td_class_other = "whitespace-nowrap px-3 py-4 text-sm text-gray-500";
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
prepareRow,
|
||||||
|
page,
|
||||||
|
canPreviousPage,
|
||||||
|
canNextPage,
|
||||||
|
pageCount,
|
||||||
|
gotoPage,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
state: { pageIndex, pageSize },
|
||||||
|
} = useTable(
|
||||||
|
{ columns, data, initialState: { pageIndex: 0, pageSize: 10 } },
|
||||||
|
usePagination
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
let tempArray = usersBackup.filter((item) =>
|
||||||
|
item.email.toUpperCase().includes(e.toUpperCase()) || item.name.toUpperCase().includes(e.toUpperCase())
|
||||||
|
);
|
||||||
|
setUsers(tempArray);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortTable = (e) => {
|
||||||
|
let userCopy = [...users];
|
||||||
|
if (e === "0") {
|
||||||
|
userCopy.sort((a, b) => (a.email > b.email ? 1 : -1));
|
||||||
|
} else if (e === "1") {
|
||||||
|
userCopy.sort((a, b) => (a.email > b.email ? -1 : 1));
|
||||||
|
} else if (e === "2") {
|
||||||
|
userCopy.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||||
|
} else if (e === "3") {
|
||||||
|
userCopy.sort((a, b) => (a.name > b.name ? -1 : 1));
|
||||||
|
} else {
|
||||||
|
console.log(`Sorry, we are out of ${e}`, e);
|
||||||
|
}
|
||||||
|
setUsers(userCopy);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InnerPageNumbers = () => {
|
||||||
|
let default_btn =
|
||||||
|
"z-10 bg-white border-gray-300 text-gray-700 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
|
||||||
|
let clicked_btn =
|
||||||
|
"z-10 bg-gray-50 border-gray-500 text-gray-600 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
|
||||||
|
let menuItems = [];
|
||||||
|
if (pageCount < 6) {
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
menuItems.push(
|
||||||
|
<button
|
||||||
|
className={pageIndex === i ? clicked_btn : default_btn}
|
||||||
|
onClick={() => gotoPage(i)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let j =
|
||||||
|
pageIndex === 0 || pageIndex === 1
|
||||||
|
? 0
|
||||||
|
: pageCount - pageIndex === 1 ||
|
||||||
|
pageCount - pageIndex === 0 ||
|
||||||
|
pageCount - pageIndex === 2
|
||||||
|
? pageCount - 5
|
||||||
|
: pageIndex - 2;
|
||||||
|
for (let i = j; i < j + 5; i++) {
|
||||||
|
menuItems.push(
|
||||||
|
<button
|
||||||
|
className={pageIndex === i ? clicked_btn : default_btn}
|
||||||
|
onClick={() => gotoPage(i)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <div>{menuItems}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatEmail = (cell) => {
|
||||||
|
if (cell.value) {
|
||||||
|
cell.column.Header = "Email"
|
||||||
|
return cell.value
|
||||||
|
} else {
|
||||||
|
cell.column.Header = "ID"
|
||||||
|
return cell.row.original.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh = (filter) => {
|
||||||
|
getUsers(getAccessTokenSilently)
|
||||||
|
.then((list) => {
|
||||||
|
setEmpty(list.length === 0);
|
||||||
|
return list;
|
||||||
|
})
|
||||||
|
.then((sorted) => {
|
||||||
|
return filter != null ? filter(sorted) : sorted;
|
||||||
|
})
|
||||||
|
.then((filtered) => {
|
||||||
|
setUsersBackup(filtered);
|
||||||
|
setUsers(filtered);
|
||||||
|
})
|
||||||
|
.then(() => setLoading(false))
|
||||||
|
.catch((error) => handleError(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh(null);
|
||||||
|
}, [getAccessTokenSilently]);
|
||||||
|
useEffect(() => {}, [users]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-10 bg-gray-50 overflow-hidden rounded max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<header className="sm:flex sm:items-center">
|
||||||
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 sm:flex-auto">
|
||||||
|
<h1 className="text-xl font-semibold text-gray-900">
|
||||||
|
Users
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-sm text-gray-700">
|
||||||
|
A list of all Users
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="px-4 sm:px-0">
|
||||||
|
{loading && <Loading />}
|
||||||
|
{error != null && <span>{error.toString()}</span>}
|
||||||
|
<main>
|
||||||
|
{loading && <Loading />}
|
||||||
|
{error != null && <span>{error.toString()}</span>}
|
||||||
|
|
||||||
|
{!empty ? (
|
||||||
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div className="flex w-full items-center mt-8 justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<input
|
||||||
|
className="text-sm rounded p-2 border border-gray-300 focus:border-gray-400 outline-none w-[300px]"
|
||||||
|
placeholder="Search..."
|
||||||
|
type="search"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSearch(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center mx-auto sm:px-6 lg:px-8">
|
||||||
|
<p className="ml-6 text-sm text-gray-700 px-4">
|
||||||
|
Sort by:
|
||||||
|
</p>
|
||||||
|
<select
|
||||||
|
className="bg-gray-50 text-sm text-gray-500 rounded p-2 border border-gray-300 focus:border-gray-400 outline-none"
|
||||||
|
onChange={(e) =>
|
||||||
|
sortTable(
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
value={0}
|
||||||
|
>
|
||||||
|
Email: Asc
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
value={1}
|
||||||
|
>
|
||||||
|
Email: Desc
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
value={2}
|
||||||
|
>
|
||||||
|
Name: Asc
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
value={3}
|
||||||
|
>
|
||||||
|
Name: Desc
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-8 sm:px-0">
|
||||||
|
{/* table */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="-my-2 sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||||
|
<table
|
||||||
|
{...getTableProps()}
|
||||||
|
className="min-w-full divide-y divide-gray-200"
|
||||||
|
>
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
{
|
||||||
|
//bg-gray-50
|
||||||
|
}
|
||||||
|
{headerGroups.map(
|
||||||
|
(
|
||||||
|
headerGroup
|
||||||
|
) => (
|
||||||
|
<tr
|
||||||
|
{...headerGroup.getHeaderGroupProps()}
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map(
|
||||||
|
(
|
||||||
|
column
|
||||||
|
) => (
|
||||||
|
<th
|
||||||
|
{...column.getHeaderProps()}
|
||||||
|
className={
|
||||||
|
"px-6 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{column.render(
|
||||||
|
"Header"
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
{...getTableBodyProps()}
|
||||||
|
className="bg-white divide-y divide-gray-200 "
|
||||||
|
>
|
||||||
|
{page.map(
|
||||||
|
(row) => {
|
||||||
|
prepareRow(
|
||||||
|
row
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
{...row.getRowProps()}
|
||||||
|
>
|
||||||
|
{row.cells.map(
|
||||||
|
(
|
||||||
|
cell
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
cell !=
|
||||||
|
null && (
|
||||||
|
<td
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
className={
|
||||||
|
cell.column.id === "email"
|
||||||
|
? td_class_email
|
||||||
|
: td_class_other
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cell
|
||||||
|
.column
|
||||||
|
.id ===
|
||||||
|
"name" &&
|
||||||
|
cell.value}
|
||||||
|
{cell
|
||||||
|
.column
|
||||||
|
.id ===
|
||||||
|
"email" && formatEmail(cell)
|
||||||
|
}
|
||||||
|
{cell
|
||||||
|
.column
|
||||||
|
.id ===
|
||||||
|
"role" &&
|
||||||
|
cell.value}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/* pagination */}
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||||
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className=" text-gray-700">
|
||||||
|
Showing{" "}
|
||||||
|
<span className="font-medium">
|
||||||
|
{pageCount ===
|
||||||
|
0
|
||||||
|
? 0
|
||||||
|
: pageIndex *
|
||||||
|
pageSize +
|
||||||
|
1}
|
||||||
|
</span>{" "}
|
||||||
|
to{" "}
|
||||||
|
<span className="font-medium">
|
||||||
|
{pageCount ===
|
||||||
|
0
|
||||||
|
? 0
|
||||||
|
: pageIndex ===
|
||||||
|
pageCount -
|
||||||
|
1
|
||||||
|
? data.length
|
||||||
|
: pageIndex *
|
||||||
|
pageSize +
|
||||||
|
pageSize}
|
||||||
|
</span>{" "}
|
||||||
|
of{" "}
|
||||||
|
<span className="font-medium">
|
||||||
|
{
|
||||||
|
data.length
|
||||||
|
}
|
||||||
|
</span>{" "}
|
||||||
|
{data.length ===
|
||||||
|
1
|
||||||
|
? "user"
|
||||||
|
: "users"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{pageCount ===
|
||||||
|
1 ||
|
||||||
|
pageCount ===
|
||||||
|
0 ? (
|
||||||
|
<div />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<nav
|
||||||
|
className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
|
||||||
|
aria-label="Pagination"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="relative inline-flex rounded-l-md items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
|
||||||
|
onClick={() =>
|
||||||
|
gotoPage(
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!canPreviousPage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
First
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
|
||||||
|
onClick={() =>
|
||||||
|
previousPage()
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!canPreviousPage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
Previous
|
||||||
|
</span>
|
||||||
|
<ChevronLeftIcon
|
||||||
|
className="h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<InnerPageNumbers />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
|
||||||
|
onClick={() =>
|
||||||
|
nextPage()
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!canNextPage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
Next
|
||||||
|
</span>
|
||||||
|
<ChevronRightIcon
|
||||||
|
className="h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="relative inline-flex rounded-r-md items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
|
||||||
|
onClick={() =>
|
||||||
|
gotoPage(
|
||||||
|
pageCount -
|
||||||
|
1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!canNextPage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Last
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 py-10">
|
||||||
|
<EmptyPeersPanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default withAuthenticationRequired(Users, {
|
||||||
|
onRedirecting: () => <Loading />,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user