This is an automated email from the ASF dual-hosted git repository. hsluoyz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
commit 2f09a1cdb5b95e1f8310fbb338b751d1da7141ba Author: Yang Luo <[email protected]> AuthorDate: Sat Aug 15 14:50:38 2026 +0800 fix(web): make the records page reach the whole window and stop it flickering The page could only ever show 200 records. getAgentRecords defaulted to that limit and the caller never passed one, so with a 1000 record window the older 800 were unreachable through the UI with no control to ask for them. The limit is now a filter like the others, carried in the URL so a view can be shared. The three second poll also raised the table's loading state on every tick, which covered the rows being read and left the refresh button spinning forever. Background refreshes are now silent, and auto refresh can be turned off. AgentsPage asked for agent:Unpatched, a key that exists in neither locale, so a successful unpatch rendered the raw key. It now uses translated phrases that read as status rather than as the internal verb. The payload block hardcoded a light grey that disappears against a dark background; it uses a translucent overlay instead. Records live only in memory, which operators discovered by losing them across a restart, so the page now says so up front. --- web/src/AgentRecordsPage.js | 63 ++++++++++++++++++++++++++++++++++++-------- web/src/AgentsPage.js | 6 ++--- web/src/locales/en/data.json | 5 ++++ web/src/locales/zh/data.json | 5 ++++ 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/web/src/AgentRecordsPage.js b/web/src/AgentRecordsPage.js index ec44637..165d89f 100644 --- a/web/src/AgentRecordsPage.js +++ b/web/src/AgentRecordsPage.js @@ -13,7 +13,7 @@ // limitations under the License. import React, {useCallback, useEffect, useState} from "react"; -import {Alert, Button, Descriptions, Input, Result, Select, Space, Table, Tag, Typography} from "antd"; +import {Alert, Button, Descriptions, Input, Result, Select, Space, Switch, Table, Tag, Typography} from "antd"; import {ReloadOutlined, RobotOutlined} from "@ant-design/icons"; import i18next from "i18next"; import {Link, useHistory, useLocation} from "react-router-dom"; @@ -27,8 +27,17 @@ function monitorAgentId(agentId) { return agentId === "codex_vscode" || agentId === "codex-vscode" ? "codex-cli" : agentId; } +// The server keeps a bounded in-memory window; these are the slices of it the +// page can ask for. Without a control here the UI could only ever reach the +// first 200 of the records the backend was holding. +const limitOptions = [200, 500, 1000, 5000]; +const defaultLimit = 200; + +// A translucent overlay rather than a fixed grey, so the payload block stays +// readable whichever theme the surrounding app is using. const payloadStyle = { - background: "#f5f5f5", + background: "rgba(127, 127, 127, 0.12)", + color: "inherit", borderRadius: 4, fontFamily: "monospace", fontSize: 12, @@ -97,20 +106,27 @@ export default function AgentRecordsPage({account}) { const eventType = search.get("eventType") || ""; const outcome = search.get("outcome") || ""; const session = search.get("session") || ""; + const limit = Number(search.get("limit")) || defaultLimit; const isAdmin = Setting.isAdminUser(account); const [agents, setAgents] = useState([]); const [records, setRecords] = useState([]); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(true); const [sessionDraft, setSessionDraft] = useState(session); - const load = useCallback(() => { + const load = useCallback((foreground = true) => { if (!isAdmin) { return; } - setLoading(true); - AgentBackend.getAgentRecords(agent, eventType, outcome, session).then(res => { + // Background refreshes must not raise the table's loading state: the poll + // below runs every few seconds and would otherwise cover the rows the + // operator is reading, and spin the refresh button permanently. + if (foreground) { + setLoading(true); + } + AgentBackend.getAgentRecords(agent, eventType, outcome, session, limit).then(res => { if (res.status === "ok") { setRecords(res.data || []); setError(""); @@ -120,9 +136,11 @@ export default function AgentRecordsPage({account}) { }).catch(err => { setError(err.message || String(err)); }).then(() => { - setLoading(false); + if (foreground) { + setLoading(false); + } }); - }, [agent, eventType, isAdmin, outcome, session]); + }, [agent, eventType, isAdmin, limit, outcome, session]); useEffect(() => { if (!isAdmin) { @@ -141,10 +159,13 @@ export default function AgentRecordsPage({account}) { return; } - load(); - const interval = setInterval(load, 3000); + load(true); + if (!autoRefresh) { + return undefined; + } + const interval = setInterval(() => load(false), 3000); return () => clearInterval(interval); - }, [isAdmin, load]); + }, [autoRefresh, isAdmin, load]); useEffect(() => { setSessionDraft(session); @@ -250,8 +271,22 @@ export default function AgentRecordsPage({account}) { <div style={{padding: "24px"}}> <Space style={{display: "flex", justifyContent: "space-between", marginBottom: 16}}> <Title level={3} style={{margin: 0}}>{i18next.t("agent:Agent Records")}</Title> - <Button icon={<ReloadOutlined />} loading={loading} onClick={load}>{i18next.t("general:Refresh")}</Button> + <Space> + <Switch + checked={autoRefresh} + checkedChildren={i18next.t("agent:Auto refresh")} + unCheckedChildren={i18next.t("agent:Auto refresh")} + onChange={setAutoRefresh} + /> + <Button icon={<ReloadOutlined />} loading={loading} onClick={() => load(true)}>{i18next.t("general:Refresh")}</Button> + </Space> </Space> + <Alert + type="info" + showIcon + message={i18next.t("agent:Records are kept in memory only and are lost when Gateway restarts")} + style={{marginBottom: 16}} + /> <Space wrap style={{marginBottom: 16}}> <Select value={agent} @@ -277,6 +312,12 @@ export default function AgentRecordsPage({account}) { onChange={value => setFilter("outcome", value)} style={{width: 160}} /> + <Select + value={limit} + options={limitOptions.map(value => ({label: i18next.t("agent:Last {count} records").replace("{count}", value), value: value}))} + onChange={value => setFilter("limit", value === defaultLimit ? "" : String(value))} + style={{width: 170}} + /> <Input.Search allowClear enterButton={i18next.t("agent:Filter")} diff --git a/web/src/AgentsPage.js b/web/src/AgentsPage.js index d6f7172..41bee33 100644 --- a/web/src/AgentsPage.js +++ b/web/src/AgentsPage.js @@ -71,11 +71,11 @@ export default function AgentsPage({account}) { setBusyKey(rowKey(record)); (patched ? AgentBackend.unpatchAgent(target) : AgentBackend.patchAgent(target)).then(res => { if (res.status === "ok") { - const success = patched ? "Unpatched" : "Patched"; + const done = patched ? i18next.t("agent:Monitoring disabled") : i18next.t("agent:Monitoring enabled"); const followup = res.data?.followup || record.followup; Setting.showMessage("success", followup - ? `${i18next.t(`agent:${success}`)} ${record.name}. ${followup}` - : `${i18next.t(`agent:${success}`)} ${record.name}`); + ? `${done}: ${record.name}. ${followup}` + : `${done}: ${record.name}`); scan(); } else { Setting.showMessage("error", res.msg || i18next.t("agent:Failed to update agent patch")); diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index 539f782..24dd332 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -14,6 +14,7 @@ "All agents": "All agents", "All event types": "All event types", "All outcomes": "All outcomes", + "Auto refresh": "Auto refresh", "Detail": "Detail", "Duration": "Duration", "Event": "Event", @@ -25,8 +26,11 @@ "First activity": "First activity", "Install Method": "Install Method", "Last activity": "Last activity", + "Last {count} records": "Last {count} records", "MCP target": "MCP target", "Model": "Model", + "Monitoring disabled": "Monitoring disabled", + "Monitoring enabled": "Monitoring enabled", "No agent records yet - patch an agent to start collecting them": "No agent records yet - patch an agent to start collecting them", "No agent sessions yet - patch an agent to start collecting them": "No agent sessions yet - patch an agent to start collecting them", "No supported agents found": "No supported agents found", @@ -38,6 +42,7 @@ "Payload": "Payload", "Prompt ID": "Prompt ID", "Records": "Records", + "Records are kept in memory only and are lost when Gateway restarts": "Records are kept in memory only and are lost when Gateway restarts", "Reported from": "Reported from", "Scan": "Scan", "Session": "Session", diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index b0d8f2f..ea47c7c 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -14,6 +14,7 @@ "All agents": "全部 Agent", "All event types": "全部事件类型", "All outcomes": "全部结果", + "Auto refresh": "自动刷新", "Detail": "详情", "Duration": "耗时", "Event": "事件", @@ -25,8 +26,11 @@ "First activity": "首次活动", "Install Method": "安装方式", "Last activity": "最后活动", + "Last {count} records": "最近 {count} 条记录", "MCP target": "MCP 目标", "Model": "模型", + "Monitoring disabled": "已关闭监控", + "Monitoring enabled": "已开启监控", "No agent records yet - patch an agent to start collecting them": "暂无 Agent 记录,请先 Patch Agent 开始采集", "No agent sessions yet - patch an agent to start collecting them": "暂无 Agent 会话,请先 Patch Agent 开始采集", "No supported agents found": "未发现支持的 Agent", @@ -38,6 +42,7 @@ "Payload": "负载", "Prompt ID": "Prompt ID", "Records": "记录", + "Records are kept in memory only and are lost when Gateway restarts": "记录仅保存在内存中,Gateway 重启后会丢失", "Reported from": "上报来源", "Scan": "探测", "Session": "会话", --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
