feat: add detail sidebar for record list page, improve token list page (#3589)

This commit is contained in:
WindSpiritSR 2025-02-16 22:01:25 +08:00 committed by GitHub
parent 26718bc4a1
commit 2a5722e45b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 141 additions and 53 deletions

View File

@ -73,7 +73,7 @@ class BaseListPage extends React.Component {
this.fetch({pagination}); this.fetch({pagination});
} }
getColumnSearchProps = dataIndex => ({ getColumnSearchProps = (dataIndex, customRender = null) => ({
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => ( filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
<div style={{padding: 8}}> <div style={{padding: 8}}>
<Input <Input
@ -126,8 +126,8 @@ class BaseListPage extends React.Component {
setTimeout(() => this.searchInput.select(), 100); setTimeout(() => this.searchInput.select(), 100);
} }
}, },
render: text => render: (text, record, index) => {
this.state.searchedColumn === dataIndex ? ( const highlightContent = this.state.searchedColumn === dataIndex ? (
<Highlighter <Highlighter
highlightStyle={{backgroundColor: "#ffc069", padding: 0}} highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
searchWords={[this.state.searchText]} searchWords={[this.state.searchText]}
@ -136,7 +136,10 @@ class BaseListPage extends React.Component {
/> />
) : ( ) : (
text text
), );
return customRender ? customRender({text, record, index}, highlightContent) : highlightContent;
},
}); });
handleSearch = (selectedKeys, confirm, dataIndex) => { handleSearch = (selectedKeys, confirm, dataIndex) => {

View File

@ -14,12 +14,12 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Switch, Table} from "antd"; import {Button, Descriptions, Drawer, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as RecordBackend from "./backend/RecordBackend"; import * as RecordBackend from "./backend/RecordBackend";
import i18next from "i18next"; import i18next from "i18next";
import moment from "moment";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import Editor from "./common/Editor";
class RecordListPage extends BaseListPage { class RecordListPage extends BaseListPage {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
@ -28,21 +28,6 @@ class RecordListPage extends BaseListPage {
this.fetch({pagination}); this.fetch({pagination});
} }
newRecord() {
return {
owner: "built-in",
name: "1234",
id: "1234",
clientIp: "::1",
timestamp: moment().format(),
organization: "built-in",
username: "admin",
requestUri: "/api/get-account",
action: "login",
isTriggered: false,
};
}
renderTable(records) { renderTable(records) {
let columns = [ let columns = [
{ {
@ -65,16 +50,13 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Client IP"), title: i18next.t("general:Client IP"),
dataIndex: "clientIp", dataIndex: "clientIp",
key: "clientIp", key: "clientIp",
width: "100px", width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps("clientIp"), ...this.getColumnSearchProps("clientIp", (row, highlightContent) => (
render: (text, record, index) => { <a target="_blank" rel="noreferrer" href={`https://db-ip.com/${row.text}`}>
return ( {highlightContent}
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${text}`}>
{text}
</a> </a>
); )),
},
}, },
{ {
title: i18next.t("general:Timestamp"), title: i18next.t("general:Timestamp"),
@ -120,28 +102,28 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Method"), title: i18next.t("general:Method"),
dataIndex: "method", dataIndex: "method",
key: "method", key: "method",
width: "110px", width: "100px",
sorter: true, sorter: true,
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: "GET", value: "GET"}, "GET", "HEAD", "POST", "PUT", "DELETE",
{text: "HEAD", value: "HEAD"}, "CONNECT", "OPTIONS", "TRACE", "PATCH",
{text: "POST", value: "POST"}, ].map(el => ({text: el, value: el})),
{text: "PUT", value: "PUT"},
{text: "DELETE", value: "DELETE"},
{text: "CONNECT", value: "CONNECT"},
{text: "OPTIONS", value: "OPTIONS"},
{text: "TRACE", value: "TRACE"},
{text: "PATCH", value: "PATCH"},
],
}, },
{ {
title: i18next.t("general:Request URI"), title: i18next.t("general:Request URI"),
dataIndex: "requestUri", dataIndex: "requestUri",
key: "requestUri", key: "requestUri",
// width: "300px", width: "200px",
sorter: true, sorter: true,
...this.getColumnSearchProps("requestUri"), ellipsis: {
showTitle: false,
},
...this.getColumnSearchProps("requestUri", (row, highlightContent) => (
<Tooltip placement="topLeft" title={row.text}>
{highlightContent}
</Tooltip>
)),
}, },
{ {
title: i18next.t("user:Language"), title: i18next.t("user:Language"),
@ -155,7 +137,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("record:Status code"), title: i18next.t("record:Status code"),
dataIndex: "statusCode", dataIndex: "statusCode",
key: "statusCode", key: "statusCode",
width: "90px", width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps("statusCode"), ...this.getColumnSearchProps("statusCode"),
}, },
@ -163,16 +145,26 @@ class RecordListPage extends BaseListPage {
title: i18next.t("record:Response"), title: i18next.t("record:Response"),
dataIndex: "response", dataIndex: "response",
key: "response", key: "response",
width: "90px", width: "220px",
sorter: true, sorter: true,
...this.getColumnSearchProps("response"), ellipsis: {
showTitle: false,
},
...this.getColumnSearchProps("response", (row, highlightContent) => (
<Tooltip placement="topLeft" title={row.text}>
{highlightContent}
</Tooltip>
)),
}, },
{ {
title: i18next.t("record:Object"), title: i18next.t("record:Object"),
dataIndex: "object", dataIndex: "object",
key: "object", key: "object",
width: "90px", width: "200px",
sorter: true, sorter: true,
ellipsis: {
showTitle: false,
},
...this.getColumnSearchProps("object"), ...this.getColumnSearchProps("object"),
}, },
{ {
@ -191,7 +183,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("record:Is triggered"), title: i18next.t("record:Is triggered"),
dataIndex: "isTriggered", dataIndex: "isTriggered",
key: "isTriggered", key: "isTriggered",
width: "140px", width: "120px",
sorter: true, sorter: true,
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
@ -204,6 +196,24 @@ class RecordListPage extends BaseListPage {
); );
}, },
}, },
{
title: i18next.t("general:Action"),
dataIndex: "action",
key: "action",
width: "80px",
sorter: true,
fixed: "right",
render: (text, record, index) => (
<Button type="link" onClick={() => {
this.setState({
detailRecord: record,
detailShow: true,
});
}}>
{i18next.t("general:Detail")}
</Button>
),
},
]; ];
if (Setting.isLocalAdminUser(this.props.account)) { if (Setting.isLocalAdminUser(this.props.account)) {
@ -220,7 +230,7 @@ class RecordListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={records} rowKey="id" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "100%"}} columns={columns} dataSource={records} rowKey="id" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Records")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Records")}&nbsp;&nbsp;&nbsp;&nbsp;
@ -229,10 +239,73 @@ class RecordListPage extends BaseListPage {
loading={this.state.loading} loading={this.state.loading}
onChange={this.handleTableChange} onChange={this.handleTableChange}
/> />
{/* TODO: Should be packaged as a component after confirm it run correctly.*/}
<Drawer
title={i18next.t("general:Detail")}
width={Setting.isMobile() ? "100%" : 640}
placement="right"
destroyOnClose
onClose={() => this.setState({detailShow: false})}
open={this.state.detailShow}
>
<Descriptions bordered size="small" column={1} layout={Setting.isMobile() ? "vertical" : "horizontal"} style={{padding: "12px", height: "100%", overflowY: "auto"}}>
<Descriptions.Item label={i18next.t("general:ID")}>{this.getDetailField("id")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Client IP")}>{this.getDetailField("clientIp")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Timestamp")}>{this.getDetailField("createdTime")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Organization")}>
<Link to={`/organizations/${this.getDetailField("organization")}`}>
{this.getDetailField("organization")}
</Link>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:User")}>
<Link to={`/users/${this.getDetailField("organization")}/${this.getDetailField("user")}`}>
{this.getDetailField("user")}
</Link>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Method")}>{this.getDetailField("method")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Request URI")}>{this.getDetailField("requestUri")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("user:Language")}>{this.getDetailField("language")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("record:Status code")}>{this.getDetailField("statusCode")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Action")}>{this.getDetailField("action")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("record:Response")}>
<Editor
value={this.getDetailField("response")}
fillHeight
fillWidth
dark
readOnly
/>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("record:Object")}>
<Editor
value={this.jsonStrFormatter(this.getDetailField("object"))}
lang="json"
fillHeight
fillWidth
dark
readOnly
/>
</Descriptions.Item>
</Descriptions>
</Drawer>
</div> </div>
); );
} }
jsonStrFormatter = str => {
try {
return JSON.stringify(JSON.parse(str), null, 2);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return str;
}
};
getDetailField = dataIndex => {
return this.state.detailRecord ? this.state.detailRecord?.[dataIndex] ?? "" : "";
};
fetch = (params = {}) => { fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText; let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder; const sortField = params.sortField, sortOrder = params.sortOrder;
@ -255,6 +328,8 @@ class RecordListPage extends BaseListPage {
}, },
searchText: params.searchText, searchText: params.searchText,
searchedColumn: params.searchedColumn, searchedColumn: params.searchedColumn,
detailShow: false,
detailRecord: null,
}); });
} else { } else {
if (res.data.includes("Please login first")) { if (res.data.includes("Please login first")) {

View File

@ -153,7 +153,7 @@ class TokenListPage extends BaseListPage {
title: i18next.t("token:Authorization code"), title: i18next.t("token:Authorization code"),
dataIndex: "code", dataIndex: "code",
key: "code", key: "code",
// width: '150px', width: "180px",
sorter: true, sorter: true,
...this.getColumnSearchProps("code"), ...this.getColumnSearchProps("code"),
render: (text, record, index) => { render: (text, record, index) => {
@ -164,7 +164,7 @@ class TokenListPage extends BaseListPage {
title: i18next.t("token:Access token"), title: i18next.t("token:Access token"),
dataIndex: "accessToken", dataIndex: "accessToken",
key: "accessToken", key: "accessToken",
// width: '150px', width: "220px",
sorter: true, sorter: true,
ellipsis: true, ellipsis: true,
...this.getColumnSearchProps("accessToken"), ...this.getColumnSearchProps("accessToken"),
@ -225,7 +225,7 @@ class TokenListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={tokens} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps} <Table scroll={{x: "100%"}} columns={columns} dataSource={tokens} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Tokens")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Tokens")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -22,6 +22,10 @@ export const Editor = (props) => {
height: "100%", height: "100%",
style: {height: "100%"}, style: {height: "100%"},
} : {}; } : {};
const fillWidth = props.fillWidth ? {
width: "100%",
style: {width: "100%"},
} : {};
let extensions = []; let extensions = [];
switch (props.lang) { switch (props.lang) {
case "javascript": case "javascript":
@ -37,13 +41,17 @@ export const Editor = (props) => {
case "xml": case "xml":
extensions = [langs.xml()]; extensions = [langs.xml()];
break; break;
case "json":
extensions = [langs.json()];
break;
} }
return ( return (
<CodeMirror <CodeMirror
value={props.value} value={props.value}
height={props.height} {...props}
{...fillHeight} {...fillHeight}
{...fillWidth}
readOnly={props.readOnly} readOnly={props.readOnly}
theme={props.dark ? materialDark : "light"} theme={props.dark ? materialDark : "light"}
extensions={extensions} extensions={extensions}

View File

@ -241,6 +241,7 @@
"Delete": "Delete", "Delete": "Delete",
"Description": "Description", "Description": "Description",
"Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it", "Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it",
"Detail": "Detail",
"Disable": "Disable", "Disable": "Disable",
"Display name": "Display name", "Display name": "Display name",
"Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI", "Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI",

View File

@ -241,6 +241,7 @@
"Delete": "删除", "Delete": "删除",
"Description": "描述信息", "Description": "描述信息",
"Description - Tooltip": "供人参考的详细描述信息Casdoor平台本身不会使用", "Description - Tooltip": "供人参考的详细描述信息Casdoor平台本身不会使用",
"Detail": "详情",
"Disable": "关闭", "Disable": "关闭",
"Display name": "显示名称", "Display name": "显示名称",
"Display name - Tooltip": "在界面里公开显示的、易读的名称", "Display name - Tooltip": "在界面里公开显示的、易读的名称",