You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@casbin.apache.org by hs...@apache.org on 2026/09/04 05:57:05 UTC
(casbin-gateway) branch master updated (194a7be -> d06bfbb)
This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
from 194a7be style(header): improve language and account menus
new feaf78c fix(agent): return a string row key for agent records
new d06bfbb feat(audit): link authenticity tiles to their provider card
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
web/src/components/AuthenticityOverview.tsx | 351 ++++++++++++++--------------
web/src/components/ProviderAuditPanel.tsx | 62 +++--
web/src/pages/AgentDetailPage.tsx | 2 +-
web/src/pages/AuthenticityPage.tsx | 4 +-
4 files changed, 229 insertions(+), 190 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org
(casbin-gateway) 02/02: feat(audit): link authenticity tiles to their provider card
Posted by hs...@apache.org.
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 d06bfbbeb713e41dc399dc592bf7ca8fcb0128ea
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Fri Sep 4 13:56:57 2026 +0800
feat(audit): link authenticity tiles to their provider card
---
web/src/components/AuthenticityOverview.tsx | 351 ++++++++++++++--------------
web/src/components/ProviderAuditPanel.tsx | 62 +++--
web/src/pages/AuthenticityPage.tsx | 4 +-
3 files changed, 228 insertions(+), 189 deletions(-)
diff --git a/web/src/components/AuthenticityOverview.tsx b/web/src/components/AuthenticityOverview.tsx
index 8d8cabd..3580fc9 100644
--- a/web/src/components/AuthenticityOverview.tsx
+++ b/web/src/components/AuthenticityOverview.tsx
@@ -1,174 +1,177 @@
-// Copyright 2026 The casbin Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import * as React from "react";
-import {Link} from "react-router-dom";
-import {ShieldCheck} from "lucide-react";
-import i18next from "i18next";
-
-import * as ProviderBackend from "@/backend/ProviderBackend";
-import {ScoreBadge, ScoreDial} from "@/components/AuthenticityScore";
-import {ProviderIcon} from "@/components/ProviderIcon";
-import {Badge} from "@/components/ui/badge";
-import {Button} from "@/components/ui/button";
-import {Card, CardContent} from "@/components/ui/card";
-import {cn} from "@/lib/utils";
-import {gradeStyleOf, probeFindings, summarizeAuthenticity} from "@/lib/authenticity";
-import {providerIdOf} from "@/lib/providers";
-import type {Provider, ProviderProbe, ProviderProbeMode} from "@/types";
-
-/** How often the scores are read again while the page is open. */
-const refreshMs = 20000;
-
-/** Fills {name} placeholders, which is all the interpolation these strings need. */
-function fill(key: string, values: Record<string, string | number>) {
- return Object.entries(values).reduce(
- (text, [name, value]) => text.split(`{${name}}`).join(String(value)),
- i18next.t(key),
- );
-}
-
-/** One provider's line: who it is, what it scored, and what failed if anything. */
-function ProviderScore({provider, probe}: {provider: Provider; probe?: ProviderProbe}) {
- const {alerts, warnings} = probeFindings(probe);
- const name = provider.displayName || provider.name;
-
- return (
- <div className="hover:border-foreground/20 flex min-w-0 items-center gap-2 rounded-lg border px-2.5 py-1.5 transition-colors">
- <ProviderIcon icon={provider.icon} baseUrl={provider.baseUrl} alt={name} size={16} />
- <span className="truncate text-xs">{name}</span>
- <div className="ml-auto flex shrink-0 items-center gap-1.5">
- {alerts.length > 0 ? (
- <Badge variant="danger">{fill("audit:Failed cases", {count: alerts.length})}</Badge>
- ) : warnings.length > 0 ? (
- <Badge variant="warning">{fill("audit:Flagged cases", {count: warnings.length})}</Badge>
- ) : null}
- <ScoreBadge probe={probe} showLabel={false} />
- </div>
- </div>
- );
-}
-
-/**
- * Whether the APIs on this machine are what they are sold as, in one card. It
- * is measured without being asked for: every provider is probed when it is
- * added and again once its report goes stale, so this says something before
- * anyone thinks to check.
- */
-export function AuthenticityOverview({
- providers,
- showLink = true,
- className,
-}: {
- providers: Provider[];
- /** False on the page this links to, where the link would point at itself. */
- showLink?: boolean;
- className?: string;
-}) {
- const [probes, setProbes] = React.useState<ProviderProbe[]>([]);
- const [mode, setMode] = React.useState<ProviderProbeMode>("auto");
- const [loaded, setLoaded] = React.useState(false);
-
- React.useEffect(() => {
- const load = () => {
- ProviderBackend.getProviderProbes()
- .then(res => {
- if (res.status !== "ok") {
- return;
- }
- setProbes(res.data ?? []);
- setMode(res.data2 ?? "auto");
- })
- .catch(() => undefined)
- .then(() => setLoaded(true));
- };
-
- load();
- const interval = setInterval(load, refreshMs);
- return () => clearInterval(interval);
- }, []);
-
- const configured = providers.filter(provider => provider.status !== "disabled");
- const byProvider = new Map(probes.map(probe => [probe.provider, probe]));
- const scored = configured
- .map(provider => ({provider: provider, probe: byProvider.get(providerIdOf(provider))}))
- .sort((left, right) => {
- const score = (entry: {probe?: ProviderProbe}) =>
- entry.probe?.ok && entry.probe.grade !== "unknown" ? entry.probe.score : Number.POSITIVE_INFINITY;
- return score(left) - score(right);
- });
-
- const summary = summarizeAuthenticity(
- scored.filter(entry => entry.probe).map(entry => entry.probe as ProviderProbe),
- configured.length,
- );
- // The headline grade is the worst one held, not the average: a machine with
- // four honest providers and one that is not is not four fifths fine.
- const worst = scored.find(entry => entry.probe?.ok && entry.probe.grade !== "unknown")?.probe;
- const style = gradeStyleOf(worst?.grade);
-
- if (configured.length === 0) {
- return null;
- }
-
- return (
- <Card className={cn("gap-0 py-0 shadow-xs", className)}>
- <CardContent className="flex flex-col gap-3 p-4">
- <div className="flex flex-wrap items-center gap-3">
- <ScoreDial probe={worst} size={56} />
- <div className="flex min-w-0 flex-col gap-0.5">
- <div className="flex items-center gap-1.5">
- <ShieldCheck className="text-muted-foreground size-3.5" />
- <span className="text-sm font-medium">{i18next.t("audit:Authenticity")}</span>
- </div>
- <span className="text-xs">
- {summary.graded === 0
- ? i18next.t(mode === "off" ? "audit:Probing is off" : "audit:Measuring now")
- : i18next.t(style.verdict)}
- </span>
- <span className="text-muted-foreground text-[11px]">
- {fill("audit:Graded providers", {
- graded: summary.graded,
- total: configured.length,
- })}
- {summary.alerting > 0 ? ` · ${fill("audit:Providers alerting", {count: summary.alerting})}` : ""}
- {summary.ungraded > 0 ? ` · ${fill("audit:Providers unmeasured", {count: summary.ungraded})}` : ""}
- </span>
- </div>
-
- {showLink ? (
- <Button asChild variant="outline" size="sm" className="ml-auto">
- <Link to="/authenticity">
- <ShieldCheck />
- {i18next.t("audit:See the evidence")}
- </Link>
- </Button>
- ) : null}
- </div>
-
- {loaded ? (
- <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
- {scored.map(entry => (
- <ProviderScore
- key={providerIdOf(entry.provider)}
- provider={entry.provider}
- probe={entry.probe}
- />
- ))}
- </div>
- ) : null}
- </CardContent>
- </Card>
- );
-}
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as React from "react";
+import {Link} from "react-router-dom";
+import {ShieldCheck} from "lucide-react";
+import i18next from "i18next";
+
+import * as ProviderBackend from "@/backend/ProviderBackend";
+import {ScoreBadge, ScoreDial} from "@/components/AuthenticityScore";
+import {ProviderIcon} from "@/components/ProviderIcon";
+import {Badge} from "@/components/ui/badge";
+import {Button} from "@/components/ui/button";
+import {Card, CardContent} from "@/components/ui/card";
+import {cn} from "@/lib/utils";
+import {gradeStyleOf, probeFindings, summarizeAuthenticity} from "@/lib/authenticity";
+import {providerIdOf} from "@/lib/providers";
+import type {Provider, ProviderProbe, ProviderProbeMode} from "@/types";
+
+/** How often the scores are read again while the page is open. */
+const refreshMs = 20000;
+
+/** Fills {name} placeholders, which is all the interpolation these strings need. */
+function fill(key: string, values: Record<string, string | number>) {
+ return Object.entries(values).reduce(
+ (text, [name, value]) => text.split(`{${name}}`).join(String(value)),
+ i18next.t(key),
+ );
+}
+
+/** One provider's line: who it is, what it scored, and what failed if anything. */
+function ProviderScore({provider, probe}: {provider: Provider; probe?: ProviderProbe}) {
+ const {alerts, warnings} = probeFindings(probe);
+ const name = provider.displayName || provider.name;
+
+ return (
+ <Link
+ to={`/authenticity?tab=report&provider=${encodeURIComponent(providerIdOf(provider))}`}
+ className="hover:border-foreground/20 hover:bg-accent/40 flex min-w-0 items-center gap-2 rounded-lg border px-2.5 py-1.5 transition-colors"
+ >
+ <ProviderIcon icon={provider.icon} baseUrl={provider.baseUrl} alt={name} size={16} />
+ <span className="truncate text-xs">{name}</span>
+ <div className="ml-auto flex shrink-0 items-center gap-1.5">
+ {alerts.length > 0 ? (
+ <Badge variant="danger">{fill("audit:Failed cases", {count: alerts.length})}</Badge>
+ ) : warnings.length > 0 ? (
+ <Badge variant="warning">{fill("audit:Flagged cases", {count: warnings.length})}</Badge>
+ ) : null}
+ <ScoreBadge probe={probe} showLabel={false} />
+ </div>
+ </Link>
+ );
+}
+
+/**
+ * Whether the APIs on this machine are what they are sold as, in one card. It
+ * is measured without being asked for: every provider is probed when it is
+ * added and again once its report goes stale, so this says something before
+ * anyone thinks to check.
+ */
+export function AuthenticityOverview({
+ providers,
+ showLink = true,
+ className,
+}: {
+ providers: Provider[];
+ /** False on the page this links to, where the link would point at itself. */
+ showLink?: boolean;
+ className?: string;
+}) {
+ const [probes, setProbes] = React.useState<ProviderProbe[]>([]);
+ const [mode, setMode] = React.useState<ProviderProbeMode>("auto");
+ const [loaded, setLoaded] = React.useState(false);
+
+ React.useEffect(() => {
+ const load = () => {
+ ProviderBackend.getProviderProbes()
+ .then(res => {
+ if (res.status !== "ok") {
+ return;
+ }
+ setProbes(res.data ?? []);
+ setMode(res.data2 ?? "auto");
+ })
+ .catch(() => undefined)
+ .then(() => setLoaded(true));
+ };
+
+ load();
+ const interval = setInterval(load, refreshMs);
+ return () => clearInterval(interval);
+ }, []);
+
+ const configured = providers.filter(provider => provider.status !== "disabled");
+ const byProvider = new Map(probes.map(probe => [probe.provider, probe]));
+ const scored = configured
+ .map(provider => ({provider: provider, probe: byProvider.get(providerIdOf(provider))}))
+ .sort((left, right) => {
+ const score = (entry: {probe?: ProviderProbe}) =>
+ entry.probe?.ok && entry.probe.grade !== "unknown" ? entry.probe.score : Number.POSITIVE_INFINITY;
+ return score(left) - score(right);
+ });
+
+ const summary = summarizeAuthenticity(
+ scored.filter(entry => entry.probe).map(entry => entry.probe as ProviderProbe),
+ configured.length,
+ );
+ // The headline grade is the worst one held, not the average: a machine with
+ // four honest providers and one that is not is not four fifths fine.
+ const worst = scored.find(entry => entry.probe?.ok && entry.probe.grade !== "unknown")?.probe;
+ const style = gradeStyleOf(worst?.grade);
+
+ if (configured.length === 0) {
+ return null;
+ }
+
+ return (
+ <Card className={cn("gap-0 py-0 shadow-xs", className)}>
+ <CardContent className="flex flex-col gap-3 p-4">
+ <div className="flex flex-wrap items-center gap-3">
+ <ScoreDial probe={worst} size={56} />
+ <div className="flex min-w-0 flex-col gap-0.5">
+ <div className="flex items-center gap-1.5">
+ <ShieldCheck className="text-muted-foreground size-3.5" />
+ <span className="text-sm font-medium">{i18next.t("audit:Authenticity")}</span>
+ </div>
+ <span className="text-xs">
+ {summary.graded === 0
+ ? i18next.t(mode === "off" ? "audit:Probing is off" : "audit:Measuring now")
+ : i18next.t(style.verdict)}
+ </span>
+ <span className="text-muted-foreground text-[11px]">
+ {fill("audit:Graded providers", {
+ graded: summary.graded,
+ total: configured.length,
+ })}
+ {summary.alerting > 0 ? ` · ${fill("audit:Providers alerting", {count: summary.alerting})}` : ""}
+ {summary.ungraded > 0 ? ` · ${fill("audit:Providers unmeasured", {count: summary.ungraded})}` : ""}
+ </span>
+ </div>
+
+ {showLink ? (
+ <Button asChild variant="outline" size="sm" className="ml-auto">
+ <Link to="/authenticity">
+ <ShieldCheck />
+ {i18next.t("audit:See the evidence")}
+ </Link>
+ </Button>
+ ) : null}
+ </div>
+
+ {loaded ? (
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
+ {scored.map(entry => (
+ <ProviderScore
+ key={providerIdOf(entry.provider)}
+ provider={entry.provider}
+ probe={entry.probe}
+ />
+ ))}
+ </div>
+ ) : null}
+ </CardContent>
+ </Card>
+ );
+}
diff --git a/web/src/components/ProviderAuditPanel.tsx b/web/src/components/ProviderAuditPanel.tsx
index 1ca9dfb..007abb5 100644
--- a/web/src/components/ProviderAuditPanel.tsx
+++ b/web/src/components/ProviderAuditPanel.tsx
@@ -13,7 +13,7 @@
// limitations under the License.
import * as React from "react";
-import {Link} from "react-router-dom";
+import {Link, useLocation} from "react-router-dom";
import {Logs, RefreshCw, Stethoscope} from "lucide-react";
import i18next from "i18next";
@@ -28,6 +28,7 @@ import {SimpleSelect} from "@/components/shared/simple-select";
import {MessageAlert} from "@/components/ui/alert";
import {Button} from "@/components/ui/button";
import {Card} from "@/components/ui/card";
+import {cn} from "@/lib/utils";
import {providerIdOf} from "@/lib/providers";
import type {
LlmAuditReport,
@@ -116,7 +117,7 @@ function buildRows(
* documented, which is the only way to learn something a real request never
* happened to show — and spends a few cents of that provider's own credit.
*/
-export function ProviderAuditPanel({owner}: {owner: string}) {
+export function ProviderAuditPanel({owner, focus = ""}: {owner: string; focus?: string}) {
const [hours, setHours] = React.useState(24 * 7);
const [report, setReport] = React.useState<LlmAuditReport | null>(null);
// Null until a listing lands. An empty list and "not asked yet" must not
@@ -204,6 +205,31 @@ export function ProviderAuditPanel({owner}: {owner: string}) {
const rows = buildRows(providers ?? [], report?.providers ?? [], probes);
+ // A card linked to from the overview is scrolled to and lit for a moment,
+ // otherwise the click lands on a page that looks unchanged.
+ const [lit, setLit] = React.useState("");
+ // Keyed on the navigation, so clicking the same tile twice scrolls again.
+ const navigation = useLocation().key;
+
+ React.useEffect(() => {
+ if (focus === "") {
+ return;
+ }
+ const scroll = (behavior: ScrollBehavior) =>
+ document
+ .querySelector(`[data-audit-provider="${CSS.escape(focus)}"]`)
+ ?.scrollIntoView({behavior: behavior, block: "start"});
+ scroll("smooth");
+ // Landed on exactly, once the panels above have loaded and moved the card.
+ const again = setTimeout(() => scroll("auto"), 600);
+ setLit(focus);
+ const off = setTimeout(() => setLit(""), 2500);
+ return () => {
+ clearTimeout(again);
+ clearTimeout(off);
+ };
+ }, [focus, navigation, rows.length, loading]);
+
return (
<div className="space-y-3">
<div className="flex flex-wrap items-start justify-between gap-2">
@@ -272,19 +298,27 @@ export function ProviderAuditPanel({owner}: {owner: string}) {
<>
<div className="space-y-3">
{rows.map(row => (
- <ProviderAuditCard
+ <div
key={row.id}
- audit={row.audit}
- probe={row.probe}
- cases={cases}
- provider={row.provider}
- providersKnown={providers !== null}
- probeMode={probeMode}
- probing={probing.includes(row.id)}
- // A provider the records still name but nothing is configured
- // for has no endpoint left to ask.
- onProbe={row.provider && probeMode !== "off" ? () => runProbe(row.id) : undefined}
- />
+ data-audit-provider={row.id}
+ className={cn(
+ "scroll-mt-20 rounded-xl transition-shadow",
+ lit === row.id ? "ring-primary/60 ring-2" : "",
+ )}
+ >
+ <ProviderAuditCard
+ audit={row.audit}
+ probe={row.probe}
+ cases={cases}
+ provider={row.provider}
+ providersKnown={providers !== null}
+ probeMode={probeMode}
+ probing={probing.includes(row.id)}
+ // A provider the records still name but nothing is configured
+ // for has no endpoint left to ask.
+ onProbe={row.provider && probeMode !== "off" ? () => runProbe(row.id) : undefined}
+ />
+ </div>
))}
</div>
<p className="text-muted-foreground text-xs">
diff --git a/web/src/pages/AuthenticityPage.tsx b/web/src/pages/AuthenticityPage.tsx
index 1c6b637..6b6121a 100644
--- a/web/src/pages/AuthenticityPage.tsx
+++ b/web/src/pages/AuthenticityPage.tsx
@@ -39,6 +39,8 @@ export default function AuthenticityPage({account}: {account: Account}) {
// The tab lives in the URL so the rail can link straight to one of them.
const [searchParams, setSearchParams] = useSearchParams();
const tab: AuthenticityTab = searchParams.get("tab") === "cases" ? "cases" : "report";
+ // Set when the overview above was clicked, so the report opens at that card.
+ const focus = searchParams.get("provider") ?? "";
const setTab = (next: AuthenticityTab) =>
setSearchParams(
previous => {
@@ -78,7 +80,7 @@ export default function AuthenticityPage({account}: {account: Account}) {
</TabsList>
</Tabs>
- {tab === "report" ? <ProviderAuditPanel owner={account.name} /> : <ProbeCaseList />}
+ {tab === "report" ? <ProviderAuditPanel owner={account.name} focus={focus} /> : <ProbeCaseList />}
</PageContainer>
);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org
(casbin-gateway) 01/02: fix(agent): return a string row key for agent records
Posted by hs...@apache.org.
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 feaf78c45e2ed05bfb075ffeb6e1ed93d774f2f3
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Fri Sep 4 13:08:41 2026 +0800
fix(agent): return a string row key for agent records
---
web/src/pages/AgentDetailPage.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/src/pages/AgentDetailPage.tsx b/web/src/pages/AgentDetailPage.tsx
index c33803f..5c392cd 100644
--- a/web/src/pages/AgentDetailPage.tsx
+++ b/web/src/pages/AgentDetailPage.tsx
@@ -607,7 +607,7 @@ export default function AgentDetailPage({account}: {account: Account}) {
<DataTable
columns={recordColumns}
dataSource={records}
- rowKey={record => record.id}
+ rowKey={record => String(record.id)}
pageSize={0}
emptyText={i18next.t("agent:Monitoring, no activity yet")}
/>
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org