Display the list of API tokens
This commit is contained in:
115
matrixgw_frontend/package-lock.json
generated
115
matrixgw_frontend/package-lock.json
generated
@@ -15,7 +15,9 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.5",
|
||||||
|
"@mui/x-data-grid": "^8.18.0",
|
||||||
"@mui/x-date-pickers": "^8.17.0",
|
"@mui/x-date-pickers": "^8.17.0",
|
||||||
|
"date-and-time": "^4.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"is-cidr": "^6.0.1",
|
"is-cidr": "^6.0.1",
|
||||||
"qrcode.react": "^4.2.0",
|
"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": {
|
"node_modules/@mui/x-date-pickers": {
|
||||||
"version": "8.17.0",
|
"version": "8.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.17.0.tgz",
|
"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"
|
"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": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
||||||
@@ -2189,6 +2295,15 @@
|
|||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.19",
|
"version": "1.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.5",
|
||||||
|
"@mui/x-data-grid": "^8.18.0",
|
||||||
"@mui/x-date-pickers": "^8.17.0",
|
"@mui/x-date-pickers": "^8.17.0",
|
||||||
|
"date-and-time": "^4.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"is-cidr": "^6.0.1",
|
"is-cidr": "^6.0.1",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material";
|
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 { QRCodeCanvas } from "qrcode.react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { APIClient } from "../api/ApiClient";
|
import { APIClient } from "../api/ApiClient";
|
||||||
@@ -9,6 +11,7 @@ import { CreateTokenDialog } from "../dialogs/CreateTokenDialog";
|
|||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { CopyTextChip } from "../widgets/CopyTextChip";
|
import { CopyTextChip } from "../widgets/CopyTextChip";
|
||||||
import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer";
|
import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer";
|
||||||
|
import { TimeWidget } from "../widgets/TimeWidget";
|
||||||
|
|
||||||
export function APITokensRoute(): React.ReactElement {
|
export function APITokensRoute(): React.ReactElement {
|
||||||
const count = React.useRef(0);
|
const count = React.useRef(0);
|
||||||
@@ -75,7 +78,9 @@ export function APITokensRoute(): React.ReactElement {
|
|||||||
ready={list !== undefined}
|
ready={list !== undefined}
|
||||||
load={load}
|
load={load}
|
||||||
errMsg="Failed to load the list of tokens!"
|
errMsg="Failed to load the list of tokens!"
|
||||||
build={() => <>{list?.length} tokens</>}
|
build={() => (
|
||||||
|
<TokensListGrid list={list!} onReload={handleRefreshTokensList} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</MatrixGWRouteContainer>
|
</MatrixGWRouteContainer>
|
||||||
);
|
);
|
||||||
@@ -117,3 +122,92 @@ function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement {
|
|||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(", ") ?? (
|
||||||
|
<span style={{ fontStyle: "italic" }}>Unrestricted</span>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "created",
|
||||||
|
headerName: "Creation",
|
||||||
|
flex: 3,
|
||||||
|
renderCell(params) {
|
||||||
|
return <TimeWidget time={params.row.created} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "last_used",
|
||||||
|
headerName: "Last usage",
|
||||||
|
flex: 3,
|
||||||
|
renderCell(params) {
|
||||||
|
return <TimeWidget time={params.row.last_used} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "max_inactivity",
|
||||||
|
headerName: "Max inactivity",
|
||||||
|
flex: 3,
|
||||||
|
renderCell(params) {
|
||||||
|
return <TimeWidget time={params.row.max_inactivity} isDuration />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "expiration",
|
||||||
|
headerName: "Expiration",
|
||||||
|
flex: 3,
|
||||||
|
renderCell(params) {
|
||||||
|
return <TimeWidget time={params.row.expiration} showDate />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "read_only",
|
||||||
|
headerName: "Read only",
|
||||||
|
flex: 2,
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (p.list.length === 0)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You do not have created any token yet!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGrid
|
||||||
|
style={{ flex: "1" }}
|
||||||
|
rows={p.list}
|
||||||
|
columns={columns}
|
||||||
|
autoPageSize
|
||||||
|
getRowId={(c) => c.id}
|
||||||
|
isCellEditable={() => false}
|
||||||
|
isRowSelectable={() => false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
86
matrixgw_frontend/src/widgets/TimeWidget.tsx
Normal file
86
matrixgw_frontend/src/widgets/TimeWidget.tsx
Normal file
@@ -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 (
|
||||||
|
<Tooltip
|
||||||
|
title={formatDateTime(
|
||||||
|
p.isDuration ? new Date().getTime() / 1000 - p.time : p.time
|
||||||
|
)}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{p.showDate
|
||||||
|
? formatDate(p.time)
|
||||||
|
: p.isDuration
|
||||||
|
? timeDiff(0, p.time)
|
||||||
|
: timeDiffFromNow(p.time)}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user