diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json
index 9caabde..cf89c4a 100644
--- a/matrixgw_frontend/package-lock.json
+++ b/matrixgw_frontend/package-lock.json
@@ -15,7 +15,9 @@
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.3.5",
"@mui/material": "^7.3.5",
+ "@mui/x-data-grid": "^8.18.0",
"@mui/x-date-pickers": "^8.17.0",
+ "date-and-time": "^4.1.0",
"dayjs": "^1.11.19",
"is-cidr": "^6.0.1",
"qrcode.react": "^4.2.0",
@@ -1043,6 +1045,66 @@
}
}
},
+ "node_modules/@mui/x-data-grid": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.18.0.tgz",
+ "integrity": "sha512-g8y5EI3TNqrimHpH/Hv6u6i04cbvsqh39Tg4bZEhGq+SDxWp42iABlUvB7p+gtXfyd+IbmpfzUQ1hOCsHlTMZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.5",
+ "@mui/x-internals": "8.18.0",
+ "@mui/x-virtualizer": "0.2.8",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.18.0.tgz",
+ "integrity": "sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.5",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@mui/x-date-pickers": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.17.0.tgz",
@@ -1131,6 +1193,50 @@
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/@mui/x-virtualizer": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.2.8.tgz",
+ "integrity": "sha512-hCkhTg3BLLbf0SIw9Cx/NHTCUmbna+P5F2V+Bcv/9XiYhfzzmhYnm68+V6vOOhKVbV3j8JKsUEqcTC9K2Jpu8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.5",
+ "@mui/x-internals": "8.18.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@mui/x-virtualizer/node_modules/@mui/x-internals": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.18.0.tgz",
+ "integrity": "sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.5",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
@@ -2189,6 +2295,15 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/date-and-time": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-4.1.0.tgz",
+ "integrity": "sha512-tFdrmBPZrR7bun6jqmlEy/dsjV2JLeUdGALfbKdB7mf0ItMNkYYklxjFE0voGg5oapIaE7WctMClkuRzyU9pig==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json
index 99ed122..f9b7daf 100644
--- a/matrixgw_frontend/package.json
+++ b/matrixgw_frontend/package.json
@@ -17,7 +17,9 @@
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.3.5",
"@mui/material": "^7.3.5",
+ "@mui/x-data-grid": "^8.18.0",
"@mui/x-date-pickers": "^8.17.0",
+ "date-and-time": "^4.1.0",
"dayjs": "^1.11.19",
"is-cidr": "^6.0.1",
"qrcode.react": "^4.2.0",
diff --git a/matrixgw_frontend/src/routes/APITokensRoute.tsx b/matrixgw_frontend/src/routes/APITokensRoute.tsx
index f6c4470..423c367 100644
--- a/matrixgw_frontend/src/routes/APITokensRoute.tsx
+++ b/matrixgw_frontend/src/routes/APITokensRoute.tsx
@@ -1,6 +1,8 @@
import AddIcon from "@mui/icons-material/Add";
import RefreshIcon from "@mui/icons-material/Refresh";
import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material";
+import type { GridColDef } from "@mui/x-data-grid";
+import { DataGrid } from "@mui/x-data-grid";
import { QRCodeCanvas } from "qrcode.react";
import React from "react";
import { APIClient } from "../api/ApiClient";
@@ -9,6 +11,7 @@ import { CreateTokenDialog } from "../dialogs/CreateTokenDialog";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { CopyTextChip } from "../widgets/CopyTextChip";
import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer";
+import { TimeWidget } from "../widgets/TimeWidget";
export function APITokensRoute(): React.ReactElement {
const count = React.useRef(0);
@@ -75,7 +78,9 @@ export function APITokensRoute(): React.ReactElement {
ready={list !== undefined}
load={load}
errMsg="Failed to load the list of tokens!"
- build={() => <>{list?.length} tokens>}
+ build={() => (
+