From 02e55758922546ee994365918c3218520200fba6 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 14 Nov 2025 09:07:22 +0100 Subject: [PATCH] Display the list of API tokens --- matrixgw_frontend/package-lock.json | 115 ++++++++++++++++++ matrixgw_frontend/package.json | 2 + .../src/routes/APITokensRoute.tsx | 96 ++++++++++++++- matrixgw_frontend/src/widgets/TimeWidget.tsx | 86 +++++++++++++ 4 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 matrixgw_frontend/src/widgets/TimeWidget.tsx 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={() => ( + + )} /> ); @@ -117,3 +122,92 @@ function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement { ); } + +function TokensListGrid(p: { + list: Token[]; + onReload: () => void; +}): React.ReactElement { + const columns: GridColDef<(typeof p.list)[number]>[] = [ + { field: "id", headerName: "ID", flex: 1 }, + { + field: "name", + headerName: "Name", + flex: 3, + }, + { + field: "networks", + headerName: "Networks restriction", + flex: 3, + renderCell(params) { + return ( + params.row.networks?.join(", ") ?? ( + Unrestricted + ) + ); + }, + }, + { + field: "created", + headerName: "Creation", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "last_used", + headerName: "Last usage", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "max_inactivity", + headerName: "Max inactivity", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "expiration", + headerName: "Expiration", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "read_only", + headerName: "Read only", + flex: 2, + type: "boolean", + }, + ]; + + if (p.list.length === 0) + return ( +
+ You do not have created any token yet! +
+ ); + + return ( + c.id} + isCellEditable={() => false} + isRowSelectable={() => false} + /> + ); +} diff --git a/matrixgw_frontend/src/widgets/TimeWidget.tsx b/matrixgw_frontend/src/widgets/TimeWidget.tsx new file mode 100644 index 0000000..253a0e0 --- /dev/null +++ b/matrixgw_frontend/src/widgets/TimeWidget.tsx @@ -0,0 +1,86 @@ +import { Tooltip } from "@mui/material"; +import { format } from "date-and-time"; +import { time } from "../utils/DateUtils"; + +export function formatDateTime(time: number): string { + const t = new Date(); + t.setTime(1000 * time); + return format(t, "DD/MM/YYYY HH:mm:ss"); +} + +export function formatDate(time: number): string { + const t = new Date(); + t.setTime(1000 * time); + return format(t, "DD/MM/YYYY"); +} + +export function timeDiff(a: number, b: number): string { + let diff = b - a; + + if (diff === 0) return "now"; + if (diff === 1) return "1 second"; + + if (diff < 60) { + return `${diff} seconds`; + } + + diff = Math.floor(diff / 60); + + if (diff === 1) return "1 minute"; + if (diff < 60) { + return `${diff} minutes`; + } + + diff = Math.floor(diff / 60); + + if (diff === 1) return "1 hour"; + if (diff < 24) { + return `${diff} hours`; + } + + const diffDays = Math.floor(diff / 24); + + if (diffDays === 1) return "1 day"; + if (diffDays < 31) { + return `${diffDays} days`; + } + + diff = Math.floor(diffDays / 31); + + if (diff < 12) { + return `${diff} month`; + } + + const diffYears = Math.floor(diffDays / 365); + + if (diffYears === 1) return "1 year"; + return `${diffYears} years`; +} + +export function timeDiffFromNow(t: number): string { + return timeDiff(t, time()); +} + +export function TimeWidget(p: { + time?: number; + isDuration?: boolean; + showDate?: boolean; +}): React.ReactElement { + if (!p.time) return <>; + return ( + + + {p.showDate + ? formatDate(p.time) + : p.isDuration + ? timeDiff(0, p.time) + : timeDiffFromNow(p.time)} + + + ); +}